CSDN竞赛8期题解

总结

这次竞赛有挺多遗憾的地方,第二道通过了六成的用例,考试时调半天也没ac,一直以为是DP的状态划分有情况没有考虑到,直到赛后看到bug讨论才知道是后台用例字符串的长度范围是1000,题目标注的是100,这种错误不仅引起了 T 2 T_2 T2的失分,还浪费了大量时间去调试影响了后面的答题。

另一个遗憾的地方是第四道,考试时通过了八成的用例,代码逻辑没多大问题。前面题目调试花了太多时间,就没时间继续调试 T 4 T_4 T4了,赛后发现只需要多循环几次就可以AC这道题了,不得不说太亏了。

这次竞赛题目质量还是不错的,如果不考虑种种bug的话。题目由两道简单题和两道中等难度题构成,简单题原则上说是可以秒的,然而出题人可能不希望我们秒掉。比如 T 1 T_1 T1,写个hash表很快就可以解决,如果提交代码的话会发现不能ac,因为题目给的输入模板有问题,输入字符串包含空格,但是模板输入使用cin读取,如果没有怀疑输入模板,那么会在这题卡一会。再比如 T 3 T_3 T3,基础的DP题,也是可以秒的,但是输入模板读取的方格上的数字是字符串类型。本来输入模板是为了简化我们作答的,但是使用字符串的输入我们还需要逐个转化为int类型,不如自己去写输入。

忽略一些bug的话,这次的四道题有三道考察DP, T 2 T_2 T2 T 4 T_4 T4题目还是不错的,尤其是 T 4 T_4 T4,DP加上基本的概率论知识,题目比较新颖。

题目列表

1.代写匿名信

题目描述

小Q想要匿名举报XX领导不务正业! 小Q害怕别人认出他的字迹。 他选择从报纸上剪裁下来英文字母组成自己的举报信。 现在小Q找来了报纸,和自己的举报信的Txt, 你能帮他确定一下是否能够完成匿名信吗?

分析

题目输入第一行是报纸的字符串,第二行是举报信的字符串。字符串中可能含有空格,所以作答时首先把输入模板代码里的cin换成getline,然后使用hash表统计出现过的字符,如果举报信的所有字符都出现过,输出“Yes“,否则输出”No“。

代码

#include <iostream>
#include <string>
#include <unordered_set>
using namespace std;
std::string solution(std::string words, std::string msg) {
	unordered_set<char> s;
	int n = words.size(),m = msg.size();
	for(int i = 0; i < n; i++) s.insert(words[i]);
	for(int i = 0; i < m; i++) {
		if(!s.count(msg[i])) return "No";
	}
	return "Yes";
}
int main() {
	std::string words;
	std::string msg;
	getline(cin,words);
	getline(cin,msg);
	std::string result = solution(words, msg);
	std::cout<<result<<std::endl;
	return 0;
}

2.小艺改编字符串

题目描述

已知字符串str. 添加至少多少字符可以使得str变成回文串。

分析

这题的经典做法是求出str与str逆序后字符串的LCS,将str的长度减去LCS的长度就是本题的答案,下面用另一种方式求解。

题目给的字符串长度是 100 100 100以内,实际用例会出现 1000 1000 1000以内的数据,所以DP时候数组长度要注意。

设f[i][j]表示将输入s从下标为i到j的字符通过添加字符变为回文串的最小操作次数。一般的减而治之可能不太好处理本题,需要从回文串的性质出发。输入字符串是s,添加字符后得到的回文串是t,既然t是回文串,那么只要其长度大于1,t两侧的字符一定是相同的。换而言之,如果输入s两侧的字符串相同,那么将s变成回文串的操作次数和将s去掉首尾两个字符后变成回文串的操作次数也是相同的。比如 s = c a b c s = cabc s=cabc,将内层 a b ab ab变成回文串可以插入一个字符 a a a得到 a b a aba aba。而对于s而言,插入a后 c a b a c cabac cabac也是回文的。

所以我们可以根据 s [ i ] s[i] s[i] s [ j ] s[j] s[j]的字符是否相同来进行状态划分:
f [ i ] [ j ] = { f [ i + 1 ] [ j − 1 ] , if  s [ i ] ==  s [ j ] m i n ( f [ i + 1 ] [ j ] , f [ i ] [ j − 1 ] ) + 1 , if  s [ i ] !=  s [ j ] f[i][j]= \begin{cases} f[i+1][j-1], & \text{if $s[i]$== $s[j]$}\\ \\ min(f[i+1][j], f[i][j-1]) + 1,& \text{if $s[i]$!= $s[j]$} \end{cases} f[i][j]= f[i+1][j1],min(f[i+1][j],f[i][j1])+1,if s[i]== s[j]if s[i]!= s[j]

如果 s [ i ] s[i] s[i]== s [ j ] s[j] s[j] f [ i ] [ j ] = f [ i + 1 ] [ j − 1 ] f[i][j] = f[i+1][j-1] f[i][j]=f[i+1][j1],表示将 s [ i ] s[i] s[i] s [ i ] s[i] s[i]的子串变为回文串的操作次数等于将 s [ i + 1 ] s[i+1] s[i+1] s [ j − 1 ] s[j-1] s[j1]变成回文串的操作次数。

如果 s [ i ] s[i] s[i]!= s [ j ] s[j] s[j] f [ i ] [ j ] = m i n ( f [ i + 1 ] [ j ] , f [ i ] [ j − 1 ] ) + 1 f[i][j] = min(f[i+1][j], f[i][j-1]) + 1 f[i][j]=min(f[i+1][j],f[i][j1])+1,表示要想将 s [ i ] s[i] s[i] s [ j ] s[j] s[j]的子串变为回文串,只需要先将 s [ i + 1 ] s[i+1] s[i+1] s [ j ] s[j] s[j]的子串变为回文串,或者先将 s [ i ] s[i] s[i] s [ j − 1 ] s[j-1] s[j1]的子串变为回文串,然后在另一端再插入一个字符即可。

本题主要考察区间DP,如果将 i i i j j j的字符串视为规模为n的原问题的输入,那么规模为 n − 1 n - 1 n1的两个子问题的输入分别是 i + 1 i+1 i+1 j j j i i i j − 1 j-1 j1的字符串,规模为 n − 2 n-2 n2的子问题的输入是 i + 1 i+1 i+1 j − 1 j-1 j1的字符串。如果 s s s左右两侧字符相同,则原问题的解可以转化为一个规模为 n − 2 n - 2 n2的子问题的解;如果不同,原问题的解则要由两个规模为 n − 1 n - 1 n1的子问题的解合并得到。总的来说就是根据输入的不同决定去走减而治之还是分而治之的道路。

最后,还有一点需要注意的是DP的状态边界, f [ i ] [ i ] = 0 f[i][i] = 0 f[i][i]=0只能表示单个字符是回文串,将其转化为回文串的操作次数是0。如果像 a a aa aa这种情况就不能由 f [ 0 ] [ 0 ] f[0][0] f[0][0] f [ 1 ] [ 1 ] f[1][1] f[1][1]转移而来了,因为两个 a a a中间没有中间字符,我们需要定义空字符串也是回文的,即 f [ i ] [ i − 1 ] = 0 f[i][i-1] = 0 f[i][i1]=0,即 f [ 1 ] [ 0 ] = 0 f[1][0] = 0 f[1][0]=0,这样一来,由于 s [ 0 ] = = s [ 1 ] s[0] == s[1] s[0]==s[1] f [ 0 ] [ 1 ] = f [ 1 ] [ 0 ] = 0 f[0][1] = f[1][0] = 0 f[0][1]=f[1][0]=0

代码

#include <iostream>
#include <string>
#include <algorithm>
#include <sstream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 1005;
int f[N][N];
int solution(std::string s) {
	memset(f,0x3f,sizeof f);
	int n = s.size();
	for(int i = 0; i < n; i++) f[i][i] = 0;
	for(int i = 1; i < n; i++) f[i][i-1] = 0;
	for(int len = 2; len <= n; len++) {
		for(int i = 0; i < n; i++) {
			int j = i + len - 1;
			if(j > n) continue;
			if(s[i] == s[j]) f[i][j] = min(f[i][j],f[i+1][j-1]);
			else f[i][j] = min(f[i][j-1],f[i+1][j]) + 1;
		}
	}
	return f[0][n-1];
}
int main() {
	std::string s;
	std::cin>>s;
	int result = solution(s);
	std::cout<<result<<std::endl;
	return 0;
}

3. 开学趣闻之美食诱惑

题目描述

小艺酱又开学了,可是在上学的路上总会有各种意想不到的美食诱惑让小艺酱迟到。 假设小艺酱家到学校是一个n*n的矩 阵。 每个格子包含一个诱惑值p,诱惑着小艺,让她迟到。 小艺位于矩阵的左上角,学校在矩阵的右下角落。 小艺想知道 自己到达学校所要经历的最小诱惑值是?

分析

数字三角形模型的简单应用,要注意的一方面是方格的开始和结束格子上也有数字,另一方面是输入模板给的方格是字符串数组,最好自己重写下输入。还需要注意下边界,只有不是第0行时才能从上面状态转移而来,不是第0列时才能从左边状态转移而来。

代码

#include <iostream>
#include <algorithm>
#include <string>
#include <sstream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 105;
int f[N][N];
int solution(int n, std::vector<std::vector<int>>& vec) {
	memset(f,0x3f,sizeof f);
	f[0][0] = vec[0][0];
	for(int i = 0; i < n; i++) {
		for(int j = 0; j < n; j++) {
			if(i) f[i][j] = min(f[i][j],f[i-1][j] + vec[i][j]);
			if(j) f[i][j] = min(f[i][j],f[i][j-1] + vec[i][j]);
		}
	}
	return f[n-1][n-1];
}
int main() {
	int n;
	std::vector<std::vector<int>> vec;
	std::cin>>n;
	int x;
	for(int i = 0; i < n; i++) {
		vector<int> t;
		for(int j = 0; j < n; j++) {
			cin>>x;
			t.push_back(x);
		}
		vec.push_back(t);
	}
	int result = solution(n,vec);
	std::cout<<result<<std::endl;
	return 0;
}

4.题目名称:争抢糖豆

题目描述

抓糖豆,小Q与小K都喜欢吃糖豆。 但是糖豆分两种,超甜糖豆和普通糖豆。 现在有w个超甜糖豆和b个普通糖豆。 小Q和 小K开始吃糖豆,他们决定谁先吃到超甜糖豆谁就获胜。 小K每次吃的时候会捏碎一颗糖豆。 小Q先吃,小Q想知道自己获胜的概率。 如果两个人都吃不到超甜糖豆小K获胜。

分析

本题相当于需要我们统计概率论里面几何分布的概率,不过加了诸多限制。假设一个人一天只能吃一颗糖豆,那么本题的意思就是第一天小 Q Q Q吃一颗,第二天小 K K K吃一颗,第三天小 K K K捏碎一颗,第四天小 Q Q Q吃一颗,…。设小 Q Q Q i i i天吃到超甜糖豆的概率是 P i P_i Pi,则小Q吃到超甜糖豆的概率 = P 1 + P 4 + P 7 + ⋯ =P_1 + P_4 + P_7 +\cdots =P1+P4+P7+

假设小 Q Q Q在第 t t t天吃到超甜糖豆,分析下前面吃糖豆的情况,前 t − 1 t - 1 t1天小 Q Q Q吃的必然是普通糖豆,否则游戏会提前结束;小 K K K吃到的也是普通糖豆,否则小 Q Q Q已经输了;唯一不确定的是小 K K K捏碎的糖豆类型,只要超甜糖豆没有被全部捏碎,小 Q Q Q就还有获胜的可能。小 Q Q Q在第 t t t天吃到超甜糖豆,前面天数两人吃到的糖豆类型是确定的,但是捏碎的糖豆类型每次都有两种可能,也就是第 3 3 3天有 2 2 2种情况,第 6 6 6天有 4 4 4种情况,第 9 9 9天有 8 8 8种情况,这就杜绝了我们用循环来模拟本题的可能。

循环不能模拟的情况很容易想到使用dfs来枚举(也可以跳过该部分直接看后面的DP思路):

void dfs(int w,int b,int cnt,double s,int k) {
	if(!w) return;//没有超甜糖豆了
	if(cnt & 1) {//小Q吃
		res += s * w / (w + b);//第cnt天吃到的概率
		s *= 1.0 * b / (w + b);
		if(b) dfs(w,b-1,cnt+1,s,k);
		else return;
	} else {
		if(!k) {
			if(!b) return;
			s *= 1.0 * b / (w + b);
			dfs(w,b-1,cnt,s,!k);
		} else {
			if(w) {//捏碎超甜糖豆
				double t = s * w / (w + b);
				dfs(w-1,b,cnt+1,t,!k);
			}
			if(b) {//捏碎普通糖豆
				double t = s * b / (w + b);
				dfs(w,b-1,cnt+1,t,!k);
			}
			if(!(w && b)) return;
		}
	}
}

虽然使用dfs来枚举思路很明确,但是本题 w w w b b b的范围都是 1000 1000 1000,这意味着可能到一千多天小 Q Q Q才吃到超甜糖果,意味着小 K K K捏碎糖果的选择过程会重复几百次, 2 100 2^{100} 2100就是一个相当大的数了,所以使用单纯的dfs很可能一个用例都不能通过。当然我们可以使用记忆化搜索来优化下dfs的过程。假设枚举过程中前 9 9 9天一共捏碎了 1 1 1枚超甜糖豆,那么第 3 、 6 、 9 3、6、9 369天都可能是捏碎的时间,而且不管是哪天捏碎的,对后续状态枚举的作用都是一样的,存在重复计算可以使用状态数组来记录。

与DFS的思路相比,DP的思路可能更加清晰。设 f [ i ] [ j ] f[i][j] f[i][j]表示第 i i i天结束还剩 j j j枚普通糖豆的概率,游戏开始有 k k k颗超甜糖豆, b b b颗普通糖豆,前 i − 1 i - 1 i1天不管是吃掉还是捏碎,每天都会固定消耗一颗糖豆,那么第 i i i天开始还剩 t = k + b − i + 1 t = k + b - i + 1 t=k+bi+1颗糖豆,其中普通糖豆 j j j颗,超甜糖豆 t − j t - j tj颗。按照一开始的划分:

i % 3 = = 1 i \% 3 == 1 i%3==1时,小 Q Q Q吃糖豆,吃到超甜糖豆的概率是 ( t − j ) / t (t - j) / t (tj)/t,再乘上上一天结束还剩 j j j颗普通糖豆的概率 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]就是小 Q Q Q在这天吃到超甜糖豆的概率了,累加进最终结果的统计即可。这天小Q吃完普通糖豆后,还剩j颗糖豆的概率是 f [ i ] [ j ] = f [ i − 1 ] [ j + 1 ] ∗ ( j + 1 ) / t f[i][j] = f[i-1][j+1] * (j+1) / t f[i][j]=f[i1][j+1](j+1)/t,表示上一天结束还剩 j + 1 j + 1 j+1个普通糖豆,这天再吃一颗普通糖豆。

i % 3 = = 2 i \% 3 == 2 i%3==2时,小K吃糖豆,一定只能吃普通糖豆,概率和小Q吃普通糖豆一样, f [ i ] [ j ] = f [ i − 1 ] [ j + 1 ] ∗ ( j + 1 ) / t f[i][j] = f[i-1][j+1] * (j+1) / t f[i][j]=f[i1][j+1](j+1)/t

i % 3 = = 3 i \% 3 == 3 i%3==3时,小K捏碎糖豆,捏碎普通糖豆概率和吃普通糖豆一样,如果捏碎的是超甜糖豆,说明上一天结束还剩 j j j颗普通糖豆,概率就是 f [ i − 1 ] [ j ] ∗ ( t − j ) / t f[i-1][j] * (t - j) / t f[i1][j](tj)/t,再加上捏碎普通糖豆的概率就是第 i i i天结束还剩 j j j颗普通糖豆的概率了。

状态边界 f [ 0 ] [ b ] = 1 f[0][b] = 1 f[0][b]=1表示游戏开始有b颗普通糖豆的概率是 1 1 1。对天数的枚举最好是最大化枚举到 k + b k + b k+b,考试时之所以只过了八成用例就是因为只枚举到了 b + 1 b + 1 b+1天,假设 k k k b b b都是 1000 1000 1000,开始小 Q Q Q和小 K K K都吃普通糖豆,中间小 K K K捏碎超甜糖豆,那么第 1500 1500 1500天吃完了 1000 1000 1000颗普通糖豆,捏碎了 500 500 500颗超甜糖豆,第 1501 1501 1501天小 Q Q Q必然会吃到超甜糖豆,游戏结束。所以游戏枚举天数尽可能的多枚举,否则会少统计概率。

代码

#include <iostream>
#include <cstdio>
#include <string>
#include <sstream>
#include <cstring>
#include <vector>
#include <cmath>
using namespace std;
double f[1005][1005];
int main() {
	std::vector<int> vec;
	std::string line_0, token_0;
	getline(std::cin >> std::ws,line_0);
	std::stringstream tokens_0(line_0);
	while(std::getline(tokens_0, token_0, ' ')) {
		vec.push_back(std::stoi(token_0));
	}
	memset(f,0,sizeof f);
	int w = vec[0], b = vec[1];
	f[0][b] = 1;
	double res = 0;
	for(int i = 1; i <= k + b; i++) {
		for(int j = b; j >= 0; j--) {
			int t = w + b - i + 1;
			if(i % 3 == 1) {
				res += f[i-1][j] * (t - j) / t;
				f[i][j] = f[i-1][j+1] * (j + 1) / t;
			} else if(i % 3 == 2) {
				f[i][j] = f[i-1][j+1] * (j + 1) / t;
			} else {
				f[i][j] += f[i-1][j+1] * (j + 1) / t;
				f[i][j] += f[i-1][j] * (t - j) / t;
			}
		}
	}
	printf("%.9lf\n",res);
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值