Codeforces Round #739 (Div. 3) 题解 完整A~F

Codeforces Round #739 (Div. 3) 题解

A. Dislike of Threes

题意

求所有不能被 3 3 3整除且个位不为 3 3 3的正整数中,第 k k k大的是多少。(关注数据范围, 1 ≤ k ≤ 1000 1\le k\le 1000 1k1000

思路

题目限定 k ≤ 1000 k\le1000 k1000,那么不妨直接预处理打表,样例里面有第 1000 1000 1000个符合题意的数是 1666 1666 1666,那么只需处理至 1666 1666 1666即可。本题使用纯暴力,每次都求一遍也可以通过。

时间复杂度

预 处 理 O ( n ) , 求 值 O ( 1 ) 预处理O(n),求值O(1) O(n)O(1)

AC代码
ProblemLangVerdictTimeMemory
A - Dislike of ThreesGNU C++17Accepted31 ms3700 KB
#include <bits/stdc++.h>

using namespace std;

vector<int> v;

void solve() {
	int n;
	scanf("%d", &n);
	printf("%d\n", v[n - 1]);				//我是从下标0开始存储的,需要减1
}

int main() {
//	freopen("in.txt", "r", stdin);
	for (int i = 1; i <= 1666; ++i) {		//预处理
		if (i % 10 != 3 && i % 3) v.push_back(i);
	}
	int t;
	scanf("%d", &t);
	while (t--) {
		solve();
	}
	return 0;
}

B. Who’s Opposite?

题意

给定一个由偶数个点组成的环,点在环上均匀分布,按顺时针依次从 1 1 1开始编号。完成编号后,每个点关于圆心对称处必定存在另一个点,例如: 6 6 6个点组成的环中, 1 1 1 4 4 4相对, 2 2 2 5 5 5相对, 3 3 3 6 6 6相对。现给定 3 3 3个互不相同的正整数 a , b , c a,b,c a,b,c,并已知编号为 a a a的点与编号为 b b b的点相对,问与 c c c相对的点编号是多少,如果这样的环不存在,输出 − 1 -1 1

思路

给出了 a a a b b b,我们就可以求出这个环上有多少个点,记环上有 2 n 2n 2n个点,那么就有 n = ∣ a − b ∣ n=|a-b| n=ab。于是就有了 3 3 3个限制条件,也就是: a , b , c a,b,c a,b,c都必须在环上,换句话说, 1 ≤ a , b , c ≤ n 1\le a,b,c\le n 1a,b,cn。若条件满足,则能够求出 c c c对面的点,其编号是 c + n c+n c+n c − n c-n cn中符合条件的一个。细致来说, c ≤ n c\le n cn时为 c + n c+n c+n c > n c>n c>n时为 c − n c-n cn

时间复杂度

O ( 1 ) O(1) O(1)

AC代码
ProblemLangVerdictTimeMemory
B - Who’s Opposite?GNU C++17Accepted31 ms3600 KB
#include <bits/stdc++.h>

using namespace std;

void solve() {
	int a, b, c;
	scanf("%d%d%d", &a, &b, &c);
	if (a > b) swap(a, b);
	int n = b - a;
	if (a > n * 2 || b > n * 2 || c > n * 2) {	//不合法情形
		printf("-1\n");
		return;
	}
	if (c - n > 0) printf("%d\n", c - n);
	else printf("%d\n", c + n);
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) {
		solve();
	}
	return 0;
}

C. Infinity Table

题意

现有一个无限大的空表格,在 ( 1 , 1 ) (1,1) (1,1)位置填入 1 1 1,然后按照以下规则填表:

  • 在第一行找到最靠左的空白格,填入下一个正整数(此时填入的是 2 2 2),然后检查左侧是否是空白格;
  • 若左侧是空白格,则向左侧填入下一个正整数;
  • 否则,向下方填入下一个正整数;
  • 若左侧没有格子了,则重新回到第一行,再次寻找最左侧空白格,开始新的一轮。(具体如下表所示)

1 2 5 10 4 3 6   ⋮ 9 8 7   ⋮ \begin{matrix}{} 1 & 2 & 5 & 10\\ 4 & 3 & 6 & \ \vdots\\ 9 & 8 & 7 & \ \vdots \end{matrix} 14923856710  

问:正整数 k k k在这个表格中,位于什么位置,用行号和列号表示。

思路

观察表格,不难发现填入 ( 1 , 2 ) → ( 2 , 2 ) → ( 2 , 1 ) (1,2)\to(2,2)\to(2,1) (1,2)(2,2)(2,1)区域的数,值域为 ( 1 2 , 2 2 ] (1^2,2^2] (12,22],填入 ( 1 , 3 ) → ( 3 , 3 ) → ( 3 , 1 ) (1,3)\to(3,3)\to(3,1) (1,3)(3,3)(3,1)区域的数,值域为 ( 2 2 , 3 2 ] (2^2,3^2] (22,32],进一步推广可以发现,填入 ( 1 , n ) → ( n , n ) → ( n , 1 ) (1,n)\to(n,n)\to(n,1) (1,n)(n,n)(n,1)区域的数,值域为 ( ( n − 1 ) 2 , n 2 ] ((n-1)^2,n^2] ((n1)2,n2],且前 n n n个数按列从上到下排布,后 n − 1 n-1 n1个数按行从右到左排布。然后只需将 k k k减去 ( n − 1 ) 2 (n-1)^2 (n1)2,按上述规则简单求值即可。

时间复杂度

O ( 1 ) O(1) O(1)

AC代码
ProblemLangVerdictTimeMemory
C - Infinity TableGNU C++17Accepted31 ms3700 KB
#include <bits/stdc++.h>

using namespace std;

void solve() {
	int n;
	scanf("%d", &n);
	int s;
	s = floor(sqrt(n - 1));	//由于我们要减掉的是严格小于n的最大完全平方数,所以需要先减1再开根,向下取整
	n -= s * s;
	if (n <= s) printf("%d %d\n", n, s + 1);
	else printf("%d %d\n", s + 1, 2 * s + 2 - n);
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) {
		solve();
	}
	return 0;
}

D. Make a Power of Two

题意

给定一个正整数 n n n,每次在十进制下可以去掉该数的某一位(若去掉某一位后,导致该数有前导0,则前导0不消失),或者在数的末尾增加一位任意数字。求至少多少次操作后,该数变为 2 2 2的整数次幂。

思路

关注 n n n的数据范围, n n n不超过 1 0 9 10^9 109,那么也就是说,最多 9 9 9次操作后,我们一定能得到一个 2 2 2的整数次幂:去掉这个数的所有位(最多操作 8 8 8次),在末尾增加一个 1 1 1。因此,最终的结果,一定不超过 1 0 18 10^{18} 1018(也就是不断在末尾添加数,加 9 9 9次)。众所周知,long long范围内 2 2 2的整数次幂的个数极其有限,因此暴力枚举结果即可。

对于每一种可能的结果,采用双指针匹配来求出需要多少次操作。我们记 x x x为目标的数, y y y为给定的数,记 x i x_i xi x x x的第 i i i位, y j y_j yj y y y的第 j j j位,根据贪心思想,如果当前匹配至 x i x_i xi y j y_j yj位置,若 x i = y j x_i=y_j xi=yj,则将这两个位置匹配, i , j i,j i,j指针同时后移,否则 i i i指针不动, j j j指针后移,由于这一位没能匹配上,原先 j j j位置的数需要删除,操作数需要加 1 1 1。全部匹配完后,多余的部分也需要加入到操作数中,也即为 x x x补全缺失的数位,为 y y y删去多余的数位。

由于要将整数按十进制位拆分,常用的方法是将整数转化为字符串,sprintf是非常好用的函数,当然用其他任何一种合理的方式实现都可以。

时间复杂度

O ( 62 ) ( 具 体 复 杂 度 不 好 算 , 但 肯 定 很 快 , 是 常 数 级 的 复 杂 度 ) O(62)(具体复杂度不好算,但肯定很快,是常数级的复杂度) O(62)()

AC代码
ProblemLangVerdictTimeMemory
D - Make a Power of TwoGNU C++17Accepted61 ms3700 KB
#include <bits/stdc++.h>

using namespace std;

char s[80][40];
char k[40];

void solve() {
	scanf("%s", k);		//直接当作字符串读入,免去数位拆分工作
	int ans = 1000;		//初始化一个比较大的值,1000足够大了
	for (int i = 0; i < 63; ++i) {
		int cur = 0, p1 = 0, p2 = 0;	//cur保存的是当前枚举的答案下至少需要多少次操作
		while (s[i][p1] != '\0' && k[p2] != '\0') {
			if (s[i][p1] == k[p2]) ++p1, ++p2;
			else ++cur, ++p2;
		}
		while (s[i][p1] != '\0') ++cur, ++p1;
		while (k[p2] != '\0') ++cur, ++p2;
		ans = min(ans, cur);
	}
	printf("%d\n", ans);
}

int main() {
//	freopen("in.txt", "r", stdin);
	for (int i = 0; i < 63; ++i) {		//2的幂,按位拆分,预处理一下就行了
		sprintf(s[i], "%lld", 1ll << i);
	}
	int t;
	scanf("%d", &t);
	while (t--) {
		solve();
	}
	return 0;
}

E. Polycarp and String Transformation

题意

现有字符串 t t t s s s,其中 t t t初始为空串, s s s初始非空,并按以下规则进行操作:

  • t t t的末尾增加 s s s,也即 t = t + s t=t+s t=t+s
  • 选择任意一个在 s s s中出现过的字母,并将 s s s中所有该字母都删去

以上操作必须按顺序执行,并不断重复操作,直到 s s s变为空串。

现给定完成了所有操作后的字符串 t t t,求原先的字符串 s s s,并求出字母的删除顺序。

如果不存在相应的答案,输出 − 1 -1 1

思路

注意:本人认为,我的做法可能不是最优解法,虽然理解并不困难,但如果有更优的做法,读者可以择优学习。如有不足,欢迎指出!

首先,我们假定我们知道 s s s的内容,设 s s s中一共有 k k k个不同的字母,并设删除顺序为 d 1 , d 2 , … , d k d_1,d_2,\dots,d_k d1,d2,,dk,这些字母分别出现了 c 1 , c 2 , … , c k c_1,c_2,\dots,c_k c1,c2,,ck次,那么在字符串 t t t中, d 1 d_1 d1出现了 1 × c 1 1\times c_1 1×c1次, d 2 d_2 d2出现了 2 × c 2 2\times c_2 2×c2次, d k d_k dk出现了 k × c k k\times c_k k×ck次~~(我觉得这应该比较容易理解,不需要证明了)~~。

利用上述性质,我们根据得到的 t t t,从后往前逆推答案。最后一个出现的字母,一定是 d k d_k dk(这毫无疑问),除去 d k d_k dk之外,最后一个出现的字母是 d k − 1 d_{k-1} dk1。于是,字母的删除顺序就可以求出来了。根据上一段中说明的,每个字母在 t t t中出现次数的结论,我们在统计了 i × c i i\times c_i i×ci之后,很容易可以求出 c i c_i ci。如果 d i d_i di t t t中的出现次数不能被 i i i整除,那么显然答案不存在,输出 − 1 -1 1即可。

由于 t t t中,最开始时一定会加入一段完整的 s s s,那么我们对前若干个字母进行统计,直到各字母的出现次数与预期的 c i c_i ci相等。这里不建议每次都将 26 26 26个字母的出现次数一一比较,这样效率太低。假定答案存在,那么 s s s的长度就等于 ∑ c i \sum c_i ci。如果在这一段中,各字母的出现次数与预期不符,则答案不存在。

最后,还需要进行最后一次确认。现在我们已经得到了 s s s d i d_i di,我们只需要根据操作规则,完整模拟字符串 t t t的生成过程,将生成的字符串与给定的比较,如果完全一致,则可以输出答案了,否则答案不存在。

这一段比较长,可能不容易理解,总体思路是模拟,只需要根据答案,再现生成过程即可。

时间复杂度

O ( n ) ( n 是 字 符 串 t 的 长 度 ) O(n)(n是字符串t的长度) O(n)(nt)

AC代码
ProblemLangVerdictTimeMemory
E - Polycarp and String TransformationGNU C++17Accepted31 ms4600 KB
#include <bits/stdc++.h>

using namespace std;
					//代码中的变量名和题目中不太一样
char s[500005];		//s相当于题目中的t
char ss[500005];	//ss用于模拟生成t
char k[100];		//k是反向存储的删除顺序
int num[100], vis[100];

void solve() {
	scanf("%s", s);
	int n = strlen(s);
	int tot = 0, cnt = 0;
	memset(num, 0, sizeof num);
	for (int i = 0; i < n; ++i) {		//统计每个字母出现了几次
		if (!num[s[i] - 'a']) ++tot;
		++num[s[i] - 'a'];
	}
	memset(vis, 0, sizeof vis);
	for (int i = n - 1; i >= 0; --i) {		//反向推出删除的顺序
		if (!vis[s[i] - 'a']) {
			if (num[s[i] - 'a'] % tot) {	//不能整除,不合法
				printf("-1\n");
				return;
			}
			k[cnt++] = s[i];	//注意这里k的存储是反向的,输出的时候要倒过来
			num[s[i] - 'a'] /= tot--;
			vis[s[i] - 'a'] = 1;
		}
	}
	int pos = 0, sum = 0;
	for (int i = 0; i < 26; ++i) sum += num[i];	//求出s的长度
	while (sum) {		//模拟生成字符串s
		if (!num[s[pos] - 'a']) {	//某个字母数量过多,多于预期,不合法
			printf("-1\n");		//不必判少于预期的情况,因为有偏少的就一定有偏多的
			return;
		}
		--num[s[pos++] - 'a'];
		--sum;
	}
	int p = 0;
	memset(vis, 0, sizeof vis);
	for (int i = cnt; i >= 0; --i) {	//模拟生成字符串t
		if (i < cnt) vis[k[i] - 'a'] = 1;
		for (int j = 0; j < pos; ++j) {
			if (!vis[s[j] - 'a']) ss[p++] = s[j];
			if (p > n) {		//生成的t过长了,需要及时剪掉,以防RE
				printf("-1\n");
				return;
			}
		}
	}
	ss[p] = '\0';
	if (strcmp(s, ss) != 0) {	//生成的t与给定的不同,不合法
		printf("-1\n");
		return;
	}
	s[pos] = '\0';		//在原字符串上修改,相应位置设置成结束符即可
	printf("%s ", s);
	for (int i = cnt - 1; i >= 0; --i) {	//反向输出删除序列
		putchar(k[i]);
	}
	putchar('\n');
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) {
		solve();
	}
	return 0;
}

F1/F2. Nearest Beautiful Number

这两道题的方法是完全一样的,这里直接讲F2的解题方法。

题意

定义一个正整数为k-beautiful:该正整数中出现过的不同数字的数量不超过 k k k

给定正整数 n n n k k k,求不小于 n n n的最小k-beautiful数。

F1与F2的区别是数据范围不同:F1: k ≤ 2 k\le2 k2,F2: k ≤ 10 k\le10 k10

思路

这其实是一个暴力搜索+分类讨论+剪枝的题目。我们对一个数按十进制位展开,一位一位的分析。

虽然是状态的转移,但其实和DP没多大关系,简简单单的搜索罢了

设状态 ( p , k , n , f ) (p,k,n,f) (p,k,n,f)

  • p p p是当前枚举到第 p p p位,假如这个数有 l e n len len位,那么 p p p将从 0 0 0(最高位)枚举至 l e n − 1 len-1 len1(最低位),当 p = l e n p=len p=len的时候,说明枚举结束了,这是一个合法的答案。
  • k k k是当前状态下,还能有多少个新的数字出现。无论什么情况,只要 k < 0 k<0 k<0,就说明这种情况不合法。
  • n n n是枚举到当前状态时,前 p p p位连起来,生成的数,用于计算。
  • f f f是当前状态的数是否已经大于给定的限制, f = 1 f=1 f=1表示已经大于给定限制,换句话说,此后的所有数,不论取几,都能满足题目要求的“不小于”的条件。

接下来是状态的转移以及剪枝:(式中 d i d_i di表示给定的数的第 i i i位,下标从 0 0 0开始)

  • k = − 1 k=-1 k=1,不合法情形,直接剪枝。

  • p = l e n p=len p=len,合法情形,更新 a n s w e r answer answer即可(取 m i n min min),然后剪枝。

  • f = 1 f=1 f=1,不再有数的大小限制,此后所有数位以尽可能小的数来填充,有以下两种情况:

    • k = 0 k=0 k=0,不能再产生新的数,只能在已经出现过的数中找最小的来填充剩余数位。
    • k > 0 k>0 k>0,还可以产生新的数,剩下的数位全部用 0 0 0填充。

    求出值后更新 a n s w e r answer answer,然后剪枝。

  • 这一位不变,也就是等于原数位 d p d_p dp,状态转移为 ( p , k , n , 0 ) → ( p + 1 , k ′ , 10 n + d p , 0 ) (p,k,n,0)\to(p+1,k',10n+d_p,0) (p,k,n,0)(p+1,k,10n+dp,0),其中的 k ′ k' k是下一步的 k k k,其值等于 k k k k − 1 k-1 k1,要判断 d p d_p dp在之前的数中是否出现过,再决定是否需要减 1 1 1

  • 这一位加 1 1 1,状态转移为 ( p , k , n , 0 ) → ( p + 1 , k ′ , 10 n + d p + 1 , 1 ) (p,k,n,0)\to(p+1,k',10n+d_p+1,1) (p,k,n,0)(p+1,k,10n+dp+1,1),有前提条件 d p ≠ 9 d_p\not=9 dp=9

  • 这一位增加至出现过的最小数字,记大于 d p d_p dp的最小出现数字为 x x x,状态转移为 ( p , k , n , 0 ) → ( p + 1 , k , 10 n + x , 1 ) (p,k,n,0)\to(p+1,k,10n+x,1) (p,k,n,0)(p+1,k,10n+x,1),有前提条件 x x x存在,注意这里是 k k k而不是 k ′ k' k,因为保证了 x x x是已经出现过的数。

以上的 a n s w e r answer answer需初始化为无穷大,经过更新后,最终求得的 a n s w e r answer answer就是答案了。状态转移只有这 3 3 3种方式,其他方式要么不合法,要么不够优秀。

虽然看起来每一个位置都有 3 3 3类情况,复杂度是指数级别,但实际上有两种情况都会转移到 f = 1 f=1 f=1的情形中去,并立刻被剪枝,因此实际上的复杂度是线性的,甚至由于一个数的位数极其有限,算法的复杂度甚至可以认为是常数级复杂度。

时间复杂度

O ( n ) ( n 极 小 ) O(n)(n极小) O(n)(n)

AC代码
ProblemLangVerdictTimeMemory
F1 - Nearest Beautiful Number (easy version)GNU C++17Accepted30 ms3700 KB
F2 - Nearest Beautiful Number (hard version)GNU C++17Accepted46 ms3700 KB
#include <bits/stdc++.h>

using namespace std;

char s[15];
int vis[10];
long long len, ans;

void dfs(int p, int k, long long n, int f) {
	if (k == -1) return;		//不合法,剪枝
	if (p >= len) {				//合法,更新答案,剪枝
		ans = min(ans, n);
		return;
	}
	if (f) {					//合法,尽可能取最小的数填充后更新答案,剪枝
		int minm;
		if (k) minm = 0;
		else {
			for (int i = 0; i < 10; ++i) {
				if (vis[i]) {
					minm = i;
					break;
				}
			}
		}
		for (int i = p; i < len; ++i) n = n * 10 + minm;
		ans = min(ans, n);
		return;
	}
	++vis[s[p] - '0'];		//标记访问痕迹,下同
	dfs(p + 1, k - (vis[s[p] - '0'] == 1), n * 10 + s[p] - '0', 0);
	--vis[s[p] - '0'];		//擦除访问痕迹,下同
	if (s[p] != '9') {
		++vis[s[p] - '0' + 1];
		dfs(p + 1, k - (vis[s[p] - '0' + 1] == 1), n * 10 + s[p] - '0' + 1, 1);
		--vis[s[p] - '0' + 1];
	}
	int fd = 0;
	for (int i = s[p] - '0' + 2; i < 10; ++i) {
		if (vis[i]) {
			fd = i;
			break;
		}
	}
	if (fd) {		//能找到比当前位大的已经出现过的数
		++vis[fd];
		dfs(p + 1, k, n * 10 + fd, 1);
		--vis[fd];
	}
}

void solve() {
	int n, k;
	scanf("%d%d", &n, &k);
	sprintf(s, "%d", n);
	len = strlen(s), ans = 2e9;
	memset(vis, 0, sizeof vis);
	dfs(0, k, 0, 0);
	printf("%lld\n", ans);
}

int main() {
//	freopen("in.txt", "r", stdin);
	int t;
	scanf("%d", &t);
	while (t--) {
		solve();
	}
	return 0;
}

后记

这一场真的好多暴力,模拟,分类讨论之类的,没有真的涉及到太多算法,但赛时F题分类讨论没讨论清楚,AK失败,气死我了。

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值