AtCoder Beginner Contest 227 题解

本文精选了几道典型的算法竞赛题目并提供了详细的解答思路,包括序列查找、预处理、枚举、二分查找、动态规划等核心算法技巧。

A. 移位

题意:一个形如 A,A+1,...,N,1,2,...,N,1,2,...,N,...A, A + 1, ..., N, 1, 2, ..., N, 1, 2, ..., N, ...A,A+1,...,N,1,2,...,N,1,2,...,N,... 的序列,问第 KKK 个数字是什么

对于 A=1A = 1A=1 的情况其实很好解决,就是 (K−1) mod N+1(K - 1) \bmod N + 1(K1)modN+1

实际上其他情况就是加了一个偏移量 AAA,所以答案就是 ((k−1) mod n+a−1) mod n+1((k - 1)\bmod n + a - 1)\bmod n + 1((k1)modn+a1)modn+1

#include<bits/stdc++.h>
using namespace std;

int n, k, a;

int main(void) {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> n >> k >> a;
    cout << ((k - 1) % n + a - 1) % n + 1 << endl;
	return 0;
}

或者特判下 k≤n−a+1k \leq n - a + 1kna+1 的情况

#include<bits/stdc++.h>
using namespace std;

int n, k, a;

int main(void) {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> n >> k >> a;
	if (k <= n - a + 1) cout << k + a - 1 << '\n';
	else { 
		cout << (k - (n - a + 1) - 1) % n + 1 << '\n';
	}
	return 0;
}

B. 预先打表

题意:给定 NNN 组询问,每个询问一个 SSS,判断是否存在两个正整数 a,ba, ba,b,满足 4ab+3a+3b=S4ab + 3a + 3b = S4ab+3a+3b=S

我们可以预处理出所有的所有可达的 SSS 值,记录在一个 setsetset 中,之后每次查询只需要查 setsetset 中有没有这个值即可

因为 1≤Si≤10001\leq S_i\leq 10001Si1000,所以 a,ba, ba,b 的范围最大是 1≤a,b≤10001\leq a, b\leq 10001a,b1000,这样枚举所有的可能的 (a,b)(a, b)(a,b) 算出 SSS 值即可

#include<bits/stdc++.h>
using namespace std;

set<int> st;
int n, x, ans;

int main(void) {
	for (int a = 1; a <= 1000; a++) {
		for (int b = 1; b <= 1000; b++) {
			st.insert(4 * a * b + 3 * a + 3 * b);
		}
	}
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> n;
	for (int i = 0; i < n; i++) {
		cin >> x;
		ans += !st.count(x);
	}
	cout << ans << endl;
	return 0;
}

C. 枚举

题意:给定 NNN,判断存在多少组正整数 (A,B,C),A≤B≤C(A, B, C), A\leq B\leq C(A,B,C),ABC,满足 ABC≤NABC\leq NABCN

先考虑枚举哪一个数

  • 枚举 AAA,只需要枚举 O(N3)O(\sqrt[3]{N})O(3N)
  • 枚举 BBB,需要枚举 O(N)O(\sqrt{N})O(N)
  • 枚举 CCC,需要枚举 O(N)O(N)O(N)

没显然我们枚举 AAA

对于一个 AAA 来说,有多少个 BC≤⌊NA⌋BC\leq \lfloor \frac{N}{A} \rfloorBCAN 呢?

我们再 O(⌊NA⌋)O(\sqrt{\lfloor\frac{N}{A}\rfloor})O(AN) 枚举 BBB

下面只需要判断有多少个 CCC[B,⌊NAB⌋][B, \lfloor \frac{N}{AB} \rfloor][B,ABN] 中即可

答案为 ⌊NAB⌋−B+1\lfloor \frac{N}{AB} \rfloor - B + 1ABNB+1

时间复杂度:O(N23)O(N^{\frac{2}{3}})O(N32)

D. 二分

题意:NNN 个部门,第 iii 个部门有 AiA_iAi 个雇员,完成一个项目需要 KKK 个不同部门的人,问最多能完成多少个项目?

直接做发现不太好做,贪心取大的话复杂度不对,所以考虑从答案入手

二分答案

这可问题可以看成:有 XXX 个项目,每个部门向尽可能多的项目里去分配人员(这无论是对项目还是对于该部门来说都是最优的),最后判断这个方案可不可行即可

需要注意二分上界不能取太大(比如 101810^{18}1018),会在计算的过程中爆 long longlong\ longlong long

时间复杂度:O(Nlog⁡(max⁡Ai))O(N\log(\max A_i))O(Nlog(maxAi))

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll

const int N = 2e5 + 5;
int n, k, a[N];

bool check(int now) {
	int sum = 0;
	for (int i = 1; i <= n; i++) {
		sum += min(now, a[i]);
	}
	return sum >= k * now;
}

signed main(void) {
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin >> n >> k;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	int l = 0, r = 2e17 / k, ans = -1;
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (check(mid)) {
			ans = mid;
			l = mid + 1;
		}
		else r = mid - 1;
	}
	cout << ans << endl;
	return 0;
}

E. 动态规划

题意:给定一个只含有 K,E,YK, E, YK,E,Y 字符的字符串,计算在最多 KKK 次两两相邻位置交换后形成的所有可能的字符串集合的大小

首先先考虑这样的一个问题:给定两个同构串 S,TS, TS,T,他们之间的最小转移距离是多少?

那肯定是贪心地从左向右匹配,找最近的换,比如:

  • S=KEYE,T=EYKES = KEYE, T = EYKES=KEYE,T=EYKE
  • 首先使得 SSS 的第一位为 EEE,找到当前距离最近的 EEE 交换一下,SSS 变为 EKYEEKYEEKYE
  • 然后是 YYYSSS 变为 EYKEEYKEEYKE

那么对于此题,一个很简单的想法是:枚举所有可能的串,计算两者之间的距离,如果这个距离 d≤Kd\leq KdK ,那么对答案产生贡献,否则忽略

但这样做的复杂度大约是 O(330)O(3^{30})O(330) 的,显然不可行,所以我们可以尝试用线性 dpdpdp 来做

dp[i][j][k][v]dp[i][j][k][v]dp[i][j][k][v] 表示使得 SSS 的前 iii 个位置通过 jjj 次移动,含有 kkkKKKvvvEEEi−k−vi - k - vikvYYY 的所有可能的字符串种类数

这个动态规划的核心点在于,我已经知道了前 iii 个字符里分别所含的 K,E,YK, E, YK,E,Y 的个数,那么剩下的字符串的状态我也知道了,因为前面都是贪心移动的,比如原来的字符串为 S=YKEES = YKEES=YKEE,我当前知道前 222 个含有 111KKK111EEE ,那么剩下的一定是 YEYEYE 而不可能是 EYEYEY

基于这个想法,我们的 dpdpdp 就变得很简单了

或许正纠结怎么优化,不用考虑这个问题,just do it!这 ∣S∣≤30|S|\leq 30S30,大胆转移就行

转移为:

  • dp[i+1][x+nxtk][j+1][k]←dp[i][x][j][k]dp[i + 1][x + nxt_k][j + 1][k]\leftarrow dp[i][x][j][k]dp[i+1][x+nxtk][j+1][k]dp[i][x][j][k]
  • dp[i+1][x+nxte][j][k+1]←dp[i][x][j][k]dp[i + 1][x + nxt_e][j][k + 1]\leftarrow dp[i][x][j][k]dp[i+1][x+nxte][j][k+1]dp[i][x][j][k]
  • dp[i+1][x+nxty][j][k]←dp[i][x][j][k]dp[i + 1][x + nxt_y][j][k]\leftarrow dp[i][x][j][k]dp[i+1][x+nxty][j][k]dp[i][x][j][k]

nxtknxt_knxtk 表示下一个字符 KKK 距离当前位置的距离,nxte,nxtynxt_e, nxt_ynxte,nxty 同理,具体细节见代码

时间复杂度:O(∣S∣6)O(|S|^6)O(S6) 常数很小

也可以预处理出给定前 iii 个位置的 K,E,YK,E, YK,E,Y 数量得到的剩下字符串,使得 dpdpdp 最内层复杂度由 O(∣S∣)O(|S|)O(S) 变为 O(1)O(1)O(1),从而使得总复杂度变为 O(∣S∣5)O(|S|^5)O(S5)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define int ll

const int N = 33;
char s[N];
int dp[N][N * N][N][N], k, n, pos, cnt, m;

signed main(void) {
	scanf("%s%d", s + 1, &m);
	n = strlen(s + 1);
	dp[0][0][0][0] = 1;
	int ck = 0, ce = 0, cy = 0;
	for (int i = 1; i <= n; i++) {
		ck += s[i] == 'K';
		ce += s[i] == 'E';
		cy += s[i] == 'Y';
	}
	for (int i = 0; i < n; i++) {
		for (int x = 0; x <= n * n; x++) {
			for (int j = 0; j <= ck; j++) {
				for (int k = 0; k <= ce; k++) {
					if (i - j - k > cy || j + k > i) continue;
					int cntk = j, cnte = k, cnty = i - j - k;
					string p; p.clear();
					for (int v = 1; v <= n; v++) {
						if (s[v] == 'K') {
							if (cntk) cntk--;
							else p += 'K';
						}
						else if (s[v] == 'E') {
							if (cnte) cnte--;
							else p += 'E';
						}
						else {
							if (cnty) cnty--;
							else p += 'Y';
						}
					}
					int nxtk = -1, nxte = -1, nxty = -1;
					for (int v = 0; v < (int) p.size(); v++) {
						if (p[v] == 'K') {
							nxtk = v; break;
						}
					}
					for (int v = 0; v < (int) p.size(); v++) {
						if (p[v] == 'E') {
							nxte = v; break;
						}
					}
					for (int v = 0; v < (int) p.size(); v++) {
						if (p[v] == 'Y') {
							nxty = v; break;
						}
					}
					if (nxtk != -1) dp[i + 1][x + nxtk][j + 1][k] += dp[i][x][j][k];
					if (nxte != -1) dp[i + 1][x + nxte][j][k + 1] += dp[i][x][j][k];
					if (nxty != -1) dp[i + 1][x + nxty][j][k] += dp[i][x][j][k];
				}
			}
		}
	}
	int ans = 0;
	for (int i = 0; i <= min(m, n * n); i++) {
		for (int j = 0; j <= n; j++) {
			for (int k = 0; k <= n; k++) {
				ans += dp[n][i][j][k];
			}
		}
	}
	cout << ans << endl;
	return 0;
}

F. 动态规划

题意:给一个 H×WH\times WH×W 的矩阵 AAA1≤Ai,j≤109,1≤H,W≤301\le A_{i,j}\le 10^9,1\le H,W\le 301Ai,j109,1H,W30

从左上角 (1,1)(1,1)(1,1) 出发,只可以向下、向右走,要求抵达右下角 (H,W)(H,W)(H,W)

计算途经前 KKKAi,jA_{i,j}Ai,j 之和为 sumsumsum1≤K<H+W1\le K< H+W1K<H+W ,要求求出 sumsumsum 的最小值

方案为dpdpdp,很容易想到的dpdpdp状态为dp[i][j][k]dp[i][j][k]dp[i][j][k]表示:抵达到(i,j)(i,j)(i,j)时的

路径前kkk大和的最小值。

但是我们发现,无法顺利地转移方程。因为当我们试图用dp[i][j][k]dp[i][j][k]dp[i][j][k]更新dp[i+1][j][k]dp[i+1][j][k]dp[i+1][j][k]时,

我们要在我们已经走过的路径中再添上Ai+1,jA_{i+1,j}Ai+1,j,从而导致路径的前kkk大和发生了变换,

这要求我们记录所有路径途径的值。

换而言之,我们的dpdpdp方案具有后效性!

为了解决dpdpdp方案的后效性,我们可以采取如此策略

枚举分界值xxx,我们认为最终方案选取的前KKK大的Ai,jA_{i,j}Ai,j的最小值即为xxx

那么,在我们转移状态的过程中只要碰到数值≥x\ge xx,我们就统计入和,并且由kkk更新到k+1k+1k+1

需要注意的是,当Ai,j=xA_{i,j}=xAi,j=x时,有统计与不统计两种情况

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define rep(i,n) for(int i = 0; i < n; i++)
void chmin(ll&a,ll b){if(a>b)a=b;}
const ll inf = 1001001001001001001;

int main(){
    int h, w, K; cin >> h >> w >> K;
    vector<vector<int>> grid(h, vector<int>(w));
    rep(i, h) rep(j, w) cin >> grid[i][j];
    ll ans = inf;
    for(auto y : grid) for(auto x : y) {
        vector<vector<vector<ll>>> dp(K + 1, vector<vector<ll>>(h, vector<ll>(w, inf)));
        if(grid[0][0] >= x) dp[1][0][0] = grid[0][0];
        if(grid[0][0] <= x) dp[0][0][0] = 0;
        rep(i, K + 1) rep(j, h) rep(k, w) {
            if(j != h - 1){
                if(i != K && grid[j + 1][k] >= x) chmin(dp[i + 1][j + 1][k], dp[i][j][k] + grid[j + 1][k]);
                if(grid[j + 1][k] <= x) chmin(dp[i][j + 1][k], dp[i][j][k]);
            }
            if(k != w - 1){
                if(i != K && grid[j][k + 1] >= x) chmin(dp[i + 1][j][k + 1], dp[i][j][k] + grid[j][k + 1]);
                if(grid[j][k + 1] <= x) chmin(dp[i][j][k + 1], dp[i][j][k]);
            }
        }
        chmin(ans, dp[K][h - 1][w - 1]);
    }
    cout << ans << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值