[atcoder]AtCoder Grand Contest 027题解

【题目链接】

https://agc027.contest.atcoder.jp/

A

【题解】

题意: 是把 x x x个糖果分给 n n n个人,一个人如果恰好分到 a i a_{i} ai个糖果就会高兴。求最多使多少个人高兴。

题解: 一定是优先满足需求小的人,特判有额外的剩余糖果。

时间复杂度: O ( N l o g N ) O(NlogN) O(NlogN)

【代码】
# include <bits/stdc++.h>
# define 	ll 		long long
using namespace std;
const int inf = 0x3f3f3f3f, INF = 0x7fffffff;
const ll  infll = 0x3f3f3f3f3f3f3f3fll, INFll = 0x7fffffffffffffffll;
int read(){
	int tmp = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9'){ if (ch == '-') fh = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9'){ tmp = tmp * 10 + ch - '0'; ch = getchar(); }
	return tmp * fh;
}
const int N = 110;
int n, sum, num[N], ans;
int main(){
	n = read(), sum = read();
	for (int i = 1; i <= n; i++) num[i] = read();
	sort(num + 1, num + n + 1);
	ans = 0;
	while (ans < n){
		if (sum >= num[ans + 1]){
			sum -= num[++ans]; 
		}
		else break;
	}
	if (ans == n && sum != 0) ans--;
	printf("%d\n", ans);
	return 0;
}

B

【题解】

题意: 一条数轴上有 n n n个垃圾,全在正半轴上。 O O O点是垃圾桶。现在有一个机器人,一开始在 O O O点。它每移动格的代价为 ( k + 1 ) 2 (k+1)^2 (k+1)2 k k k为当前携带的垃圾数量。在移动到与垃圾坐标相同时可以捡起垃圾,每次捡起/扔掉垃圾都要 x x x的代价(不能再非 O O O点扔垃圾)。求最小代价。

题解: 考试时以为一次一定取相邻的一段,这显然是错的。

考虑贪心,行走的路线一定是先是走到最远的点,在回来时不断带上其他的点。

一个点(坐标p),如果在一趟路程中是第 i i i个取完的,那么它的代价是:

( i ∗ 2 + 3 ) ∗ p (i*2+3)*p (i2+3)p如果 i = 1 i=1 i=1

( i ∗ 2 + 1 ) ∗ p (i*2+1)*p (i2+1)p如果 i &gt; 1 i&gt;1 i>1

所以远的点被取的位置一定在近的点之前。

那么我们枚举一共走的次数,那么取的顺序是唯一确定的。

于是就有了一个 O ( N 2 ) O(N^2) O(N2)的做法。

考虑优化由于 ⌊ i / j ⌋ \lfloor i/j\rfloor i/j只有 i \sqrt{i} i 种取值,所以所有的垃圾被取的位置只会变换 N l o g N NlogN NlogN次。

时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)

【代码】
# include <bits/stdc++.h>
# define 	ll 		long long
using namespace std;
const int inf = 0x3f3f3f3f, INF = 0x7fffffff;
const ll  infll = 0x3f3f3f3f3f3f3f3fll, INFll = 0x7fffffffffffffffll;
int read(){
	int tmp = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9'){ if (ch == '-') fh = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9'){ tmp = tmp * 10 + ch - '0'; ch = getchar(); }
	return tmp * fh;
}
const int N = 201000;
int n;
ll x, p[N], ans = infll, sum[N];
int main(){
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
	n = read(), x = read();
	for (int i = 1; i <= n; i++) p[i] = read();
	int lim = n / 1000;
	sum[0] = n * x;
	for (int i = 1; i <= n; i++) sum[i] = x; 
	sum[1] += p[n] * 5; 
	for (int i = 1; i <= n; i++){
		int id = n - i + 1, nxt = 1, k = i - 1;
		bool flag = false;
		for (int j = 1; j <= k; j = nxt + 1){
			nxt = k / (k / j);
			int w = k / j + 1;
			if (j != 1 && flag == true){
				int las = k / (j - 1) + 1;
				sum[j] = sum[j] - p[id] * (2 * las + 1);
			}
			if (j >= lim || k / j == 1){
				sum[j] = sum[j] + p[id] * (2 * w + 1);
				flag = true;
			}
		} 
	} 
	for (int i = 1; i <= n; i++){
		sum[i] += sum[i - 1]; 
		if (i >= lim) ans = min(ans, sum[i]);
	}
	printf("%lld\n", ans);
	return 0;
}

C

【题解】

题意: 有一个图,每个节点有一个字母 a a a b b b,在这个图上,一条路径表示一个字符串(把点上的字母连起来)。现在问这个图上的所有路径是否能表示所有的只用 a a a b b b构成的字符串。

题解: 原问题等价于寻找一个由重复的 a − a − b − b a-a-b-b aabb构成的环,即这个环上每个点都与环上的一个 a a a与一个 b b b相邻。

考虑把一个节点拆成两种,一种是下一步要与当前点不同,另一种是下一步要与当前点相同。然后在新图上跑tarjan,如果有环,则有解。

时间复杂度 O ( N + M ) O(N+M) O(N+M)

【代码】
# include <bits/stdc++.h>
# define 	ll 		long long
using namespace std;
const int inf = 0x3f3f3f3f, INF = 0x7fffffff;
const ll  infll = 0x3f3f3f3f3f3f3f3fll, INFll = 0x7fffffffffffffffll;
int read(){
	int tmp = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9'){ if (ch == '-') fh = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9'){ tmp = tmp * 10 + ch - '0'; ch = getchar(); }
	return tmp * fh;
}
const int N = 200010;
struct Edge{
	int data, next;
}e[N * 2];
int use[N][2], head[N], flag, n, m, place;
char s[N];
void build(int u, int v){
	e[++place].data = v; e[place].next = head[u]; head[u] = place;
}
void dfs(int x, int tag){
	use[x][tag] = 1;
	for (int ed = head[x]; ed != 0; ed = e[ed].next){
		if (use[e[ed].data][tag ^ 1] == 2) continue;
		if (tag == 1){
			if (s[e[ed].data] == s[x]) continue;
			if (use[e[ed].data][0] == false)
				dfs(e[ed].data, 0);
				else flag = true;
		}
		else {
			if (s[e[ed].data] != s[x]) continue;
			if (use[e[ed].data][1] == false)
				dfs(e[ed].data, 1);
				else flag = true;
		}
	}
	use[x][tag] = 2;
}
int main(){
//	freopen(".in", "r", stdin);
//	freopen(".out", "w", stdout);
	n = read(), m = read();
	scanf("\n%s", s + 1);
	for (int i = 1; i <= m; i++){
		int u = read(), v = read();
		build(u, v);
		build(v, u);
	}
	for (int i = 1; i <= n; i++){
		if (use[i][0] == 0) dfs(i, 0);
		if (use[i][1] == 0) dfs(i, 0); 
	}
	if (flag)
		printf("Yes\n");
		else printf("No\n");
	return 0;
}

D

【题解】

题面: 构造一个 n ∗ n n*n nn的矩阵,其中的每一个数不能超过 1 0 1 5 10^15 1015。使得存在一个数 x x x使任意相邻的两个数中 大 % 小 = x 大\%小=x %=x

题解: 考虑·一个 x = 1 x=1 x=1的情况,若所有 ( x + y ) % 2 = 0 (x+y)\%2=0 (x+y)%2=0的格子中各填一个不同的素数,在其他格子中填相邻四个数的乘积(lcm)+1。那么这一定是一个合法解。但是这样做值域不够。

考虑 ( x + y ) % 2 = 0 (x+y)\%2=0 (x+y)%2=0的格子,对于左上-右下的对角线我们用前500个素数,左下-右上的对角线我们用第501到第1000个素数。格子中的数为对应的两条对角线上对应质数的积。那么每个格子的值一定不同。其他格子仍然为相邻四格的lcm,由于相邻四格中相邻两个一定有一条对角线相同,所以lcm为 相 邻 四 格 的 乘 积 \sqrt{相邻四格的乘积}

这样的话,格子中最大的数不会超过4个第1000个质数的乘积,符合条件。

【代码】
# include <bits/stdc++.h>
# define 	ll 		long long
using namespace std;
const int inf = 0x3f3f3f3f, INF = 0x7fffffff;
const ll  infll = 0x3f3f3f3f3f3f3f3fll, INFll = 0x7fffffffffffffffll;
int read(){
	int tmp = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9'){ if (ch == '-') fh = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9'){ tmp = tmp * 10 + ch - '0'; ch = getchar(); }
	return tmp * fh;
}
const int N = 100010;
ll mp[520][520];
int p[N], pnum;
bool use[N];
int n;
void prep(int n){
	use[1] = true;
	for (int i = 2; i <= n; i++){
		if (use[i] == false) p[++pnum] = i;
		for (int j = 1; 1ll * i * p[j] <= n && j <= pnum; j++){
			use[p[j] * i] = true;
			if (i % p[j] == 0) break;
		}
	}
}
int main(){
	n = read();
	prep(100000);
	for (int i = 1; i <= n + 2; i++)
		for (int j = 1; j <= n + 2; j++)
			if ((i + j) % 2 == 0){
				int p1 = (i + j) / 2, p2 = (i - j) / 2 + 760;
				mp[i][j] = p[p1] * p[p2];
			} 
	for (int i = 2; i <= n + 1; i++)
		for (int j = 2; j <= n + 1; j++)
			if ((i + j) % 2 == 1){
				mp[i][j] = mp[i - 1][j] * mp[i + 1][j];
				mp[i][j] += 1;
			}
	for (int i = 2; i <= n + 1; i++)
		for (int j = 2; j <= n + 1; j++)
			printf("%lld%c", mp[i][j], (j == n + 1) ? '\n' : ' ');
	return 0;
}

E

【题解】

题面: 给定串 S S S ∣ S ∣ ≤ 2 e 5 |S|\leq 2e5 S2e5), S S S中包含 a , b a,b a,b,有两种操作:

1.将连续的两个 a a a变成一个 b b b

2.将连续的两个 b b b变成一个 a a a

可以在任意时刻终止操作,求最后的串有几种可能。

题解: S i , j S_{i,j} Si,j表示 S S S i i i j j j的子串。

不妨把 a a a看做 1 1 1,把 b b b看做 2 2 2。记 P i , j P_{i,j} Pi,j表示 ( ∑ k = i j S k ) % 3 (\sum_{k=i}^{j}S_{k})\%3 (k=ijSk)%3

那么 S i , j S_{i,j} Si,j能转化为一个字符 x x x的条件是:

1. i = j i=j i=j S i , j S_{i,j} Si,j中有相邻两个字符相同。

2. P i , j = P x P_{i,j}=P_x Pi,j=Px

条件1保证可以进行操作,2保证最后变成的数相同。

如果我们想要得到一个串 T T T,我们尽量取最小的前缀去得到它。具体来讲,用尽量少的字母得到 T T T中第一个字符,以此类推。

那么最后要么无法组成 T T T,要么刚好组成,要么会剩下一段 S S S的后缀 S i , n S_{i,n} Si,n。给出结论:

如果 P i , n = 0 P_{i,n}=0 Pi,n=0 S S S中存在两个相邻相同的字符,那么 T T T可以被构成。

讲一个简略的证明:

设构成 T T T的最后一个字符的一段为 S j , i − 1 S_{j,i-1} Sj,i1

如果 S j , n S_{j,n} Sj,n有连续两个相同的字符,那么这一段可以简单合并为 T T T的最后一个字符。

如果没有说明 j = i − 1 j=i-1 j=i1 S j , n S_{j,n} Sj,n字符两两交替出现。

k k k S j = S j + 1 S_{j}=S_{j+1} Sj=Sj+1 k k k的最大值。字符 y y y k k k所在一段在 T T T中构成的字符。

分为两种情况:

1. k k k单独构成 y y y。由于 P i , n = 0 P_{i,n}=0 Pi,n=0所以 T T T的末尾字符等于 S S S的末尾字符。所以一直往后合并,一定有一个时刻相同。

2. k k k不单独构成 y y y,先把当前段合并到只剩两个字符,再一直往后合并即可。

证毕。

接下来就是一个简单的dp了。记 f i f_{i} fi表示 S S S中的前 i i i位能恰好构成多少种 T T T。枚举下一个字符可行的最短距离,用set维护即可。

时间复杂度 O ( N l o g N ) O(NlogN) O(NlogN)

【代码】
# include <bits/stdc++.h>
# define 	ll 		long long
using namespace std;
const int inf = 0x3f3f3f3f, INF = 0x7fffffff;
const ll  infll = 0x3f3f3f3f3f3f3f3fll, INFll = 0x7fffffffffffffffll;
int read(){
	int tmp = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9'){ if (ch == '-') fh = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9'){ tmp = tmp * 10 + ch - '0'; ch = getchar(); }
	return tmp * fh;
}
const int N = 200010, P = 1e9 + 7;
char s[N];
int num[N], f[N][3][2], n, tag[N], pre[N], ans, sam[N];
set <int> mp[3];
int main(){
	scanf("\n%s", s + 1);
	n = strlen(s + 1);
	for (int i = 1; i <= n; i++) num[i] = s[i] - 'a' + 1;
	for (int i = 1; i <= n; i++){
		if (sam[i - 1] >= i) sam[i] = sam[i - 1];
		else {
			sam[i] = n;
			for (int j = i; j < n; j++)
				if (num[j] == num[j + 1]){
					sam[i] = j;
					break;
				}
		}
	}
	for (int i = n; i >= 1; i--) tag[i] = tag[i + 1] | (num[i] == num[i - 1]); 
	for (int i = 1; i <= n; i++){
		pre[i] = (pre[i - 1] + num[i]) % 3;
		mp[pre[i]].insert(i);
	}
	mp[0].insert(n + 1), mp[1].insert(n + 1), mp[2].insert(n + 1);
	
	int cnt = 1;
	int j = *mp[1].upper_bound(sam[1]);
	if (num[1] == 1) j = 1;
	if (j <= n){
		if (j > 1) f[j][1][0] = (f[j][1][0] + cnt) % P;
			else f[j][1][1] = (f[j][1][1] + cnt) % P;
	}
	j = *mp[2].upper_bound(sam[1]);
	if (num[1] == 2) j = 1;
	if (j <= n){
		if (j > 1) f[j][2][0] = (f[j][2][0] + cnt) % P;
			else f[j][2][1] = (f[j][2][1] + cnt) % P;
	}
	
	for (int i = 1; i < n; i++){
		for (int j = 1; j <= 2; j++){
			int cnt = (f[i][j][0] + f[i][j][1]) % P;
			int k = *mp[(pre[i] + 1) % 3].upper_bound(sam[i + 1]);
			if (num[i + 1] == 1) k = i + 1;
			if (k <= n){
				if (k > i + 1 || j == 1) f[k][1][0] = (f[k][1][0] + cnt) % P;
					else f[k][1][1] = (f[k][1][1] + cnt) % P;
			}
			k = *mp[(pre[i] + 2) % 3].upper_bound(sam[i + 1]);
			if (num[i + 1] == 2) k = i + 1;
			if (k <= n){
				if (k > i + 1 || j == 2) f[k][2][0] = (f[k][2][0] + cnt) % P;
					else f[k][2][1] = (f[k][2][1] + cnt) % P;
			}
		}
	}
	
	for (int i = 1; i <= n; i++)
		if (pre[n] - pre[i] == 0){
			ans = ((ans + f[i][1][1]) % P + f[i][2][1]) % P;
			ans = ((ans + f[i][1][0]) % P + f[i][2][0]) % P;
		}
	if (sam[1] == n)
		printf("%d\n", 1);
		else printf("%d\n", ans);
	return 0;
}

F

【题解】

题面: 给定两棵树 A A A B B B,有 n n n个节点。每次可以选择 A A A中的一个叶子节点,把与它相连的边删去。再任意选择一个节点与它连边。一个点只能被操作一次。求出把 A A A变为 B B B的最小步数。 n ≤ 50 n\leq 50 n50

题解: 先考虑一种简单的情况: A A A中含有不动点,即不会被操作的点。

那么我们固定这个点 x x x,再找出这个点 A A A B B B x x x为根的最大相同子树。在这个子树上的点都是不能被操作的。其他的点一定要操作。如果一个需要操作的点 u u u,它一开始与 v v v相连,在 B B B中它的父亲为 v ′ v&#x27; v,记 t i t_{i} ti i i i号节点被操作的时间,那么以下两个条件需要被满足:

1.若 v ′ v&#x27; v为需要操作的点,那么 t v ′ &lt; t u t_{v&#x27;}&lt;t_{u} tv<tu

2.若 v v v为需要操作的节点,那么 t u &lt; t v t_{u}&lt;t_{v} tu<tv

若存在满足条件的拓扑序,那么 A A A可以变为 B B B。步数就是需要操作的点的数目。

接下来考虑没有不动点的情况,我们枚举第一步选的叶子节点及它连的边。由于每个点只能操作一次,那么这个点在这之后一定是不动点。用之前的算法就可以了。

时间复杂度:枚举 O ( N 2 ) O(N^2) O(N2),判断 O ( N ) O(N) O(N)。总复杂度 O ( N 3 ) O(N^3) O(N3)

【代码】
# include <bits/stdc++.h>
# define 	ll 		long long
using namespace std;
const int inf = 0x3f3f3f3f, INF = 0x7fffffff;
const ll  infll = 0x3f3f3f3f3f3f3f3fll, INFll = 0x7fffffffffffffffll;
int read(){
	int tmp = 0, fh = 1; char ch = getchar();
	while (ch < '0' || ch > '9'){ if (ch == '-') fh = -1; ch = getchar(); }
	while (ch >= '0' && ch <= '9'){ tmp = tmp * 10 + ch - '0'; ch = getchar(); }
	return tmp * fh;
}
const int N = 61;
vector <int> ea[N], eb[N], e[N];
int ua[N], va[N], ub[N], vb[N], deg[N], use[N], cnt, da[N], db[N], tag[N], n, q[N]; 
void dfs(int x, int fa){
	use[x] = true; cnt++;
	for (unsigned eda = 0, edb = 0; eda < ea[x].size() && edb < eb[x].size(); ){
	//	printf("%d %d\n", ea[x][eda], eb[x][edb]);
		if (ea[x][eda] == eb[x][edb] && ea[x][eda] != fa) dfs(ea[x][eda], x);
		if (edb >= eb[x].size() - 1 || (eda < ea[x].size() - 1 && ea[x][eda + 1] <= eb[x][edb + 1]))
			eda++; else edb++; 
	}
}
void foda(int x, int fa){
	da[x] = fa;
	for (unsigned ed = 0; ed < ea[x].size(); ed++)
		if (ea[x][ed] != fa) foda(ea[x][ed], x);
}
void fodb(int x, int fa){
	db[x] = fa;
	for (unsigned ed = 0; ed < eb[x].size(); ed++)
		if (eb[x][ed] != fa) fodb(eb[x][ed], x);
}
int check(int x){
	memset(use, 0, sizeof(use)); memset(tag, 0, sizeof(tag));
	for (int i = 1; i <= n; i++) ea[i].clear(), eb[i].clear(), e[i].clear();
	for (int i = 1; i < n; i++){
		ea[ua[i]].push_back(va[i]);
		ea[va[i]].push_back(ua[i]);
	}
	for (int i = 1; i < n; i++){
		eb[ub[i]].push_back(vb[i]);
		eb[vb[i]].push_back(ub[i]);
	}
	for (int i = 1; i <= n; i++){
		sort(ea[i].begin(), ea[i].end());
		sort(eb[i].begin(), eb[i].end());
	}
	cnt = 0; dfs(x, 0);
	cnt = n - cnt;
	foda(x, 0); fodb(x, 0);
	for (int i = 1; i <= n; i++){
		if (use[i] == true) continue;
		if (use[da[i]] == false) e[i].push_back(da[i]), tag[da[i]]++;
		if (use[db[i]] == false) e[db[i]].push_back(i), tag[i]++;
	}
	int pl = 1, pr = 0, tmp = 0;
	for (int i = 1; i <= n; i++) if (tag[i] == 0) q[++pr] = i; 
	while (pl <= pr){
		int x = q[pl++]; tmp++;
		for (unsigned i = 0; i < e[x].size(); i++){
			tag[e[x][i]]--;
			if (tag[e[x][i]] == 0) q[++pr] = e[x][i];
		}
	}
	if (tmp == n) return cnt;
		else return inf;
}
int main(){
	for (int opt = read(); opt--; ){
		n = read();
		memset(deg, 0, sizeof(deg));
		for (int i = 1; i < n; i++){
			ua[i] = read(), va[i] = read();
			deg[ua[i]]++; deg[va[i]]++;
		}
		for (int i = 1; i < n; i++)
			ub[i] = read(), vb[i] = read();
		int flag = inf;
		for (int i = 1; i <= n; i++)
			flag = min(check(i), flag);
		for (int i = 1; i <= n; i++){
			if (deg[i] != 1) continue;
			for (int j = 1; j <= n; j++){
				if (i == j) continue;
				for (int k = 1; k < n; k++){
					if (ua[k] == i){
						int tmp = va[k];
						va[k] = j;
						flag = min(flag, check(i) + 1);
						va[k] = tmp;
						break;
					}
					if (va[k] == i){
						int tmp = ua[k];
						ua[k] = j;
						flag = min(flag, check(i) + 1);
						ua[k] = tmp;
						break;
					}
				}
			}
		}
		if (flag == inf)
			printf("-1\n");
			else printf("%d\n", flag); 
	} 
	return 0;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值