CSP-S 2023 T1密码锁 T2消消乐

T1 密码锁

对于一种状态,其可能产生的其他状态共有两种情况,一种情况是只转一个拨圈,这样产生的密码共有 5 ∗ 9 = 45 5*9=45 59=45种,另一种情况是转相邻的两个拨圈,这样产生的密码共有 4 ∗ 9 = 36 4*9=36 49=36种,所以一种状态能产生的密码共有 45 + 36 = 81 45+36=81 45+36=81种。现在题目中给出了 n ( n ≤ 8 ) n(n\leq 8) n(n8)个状态,问能满足所有这些状态的密码有多少种。
由于密码只有 5 5 5位,所以总共的情况也只有 1 0 5 10^5 105种,对于每种密码,我们只需要判断它是不是能到达给出的 n n n个状态。判断时,我们遍历 n n n个状态,对每个状态分别进行转一个拨圈的变换和转两个拨圈的变换,看是否能够得到该密码,若 n n n个状态都符合条件,则答案加一。复杂度是完全可以接受的。

再讲一种复杂度较小的做法。对于每种状态 ( n ≤ 8 ) (n\leq 8) (n8),枚举这个状态所能到达的所有密码( 81 81 81种),记录每种密码被枚举到的次数。枚举完 n n n个状态能到达的密码后,遍历所有密码,若这个密码被枚举到的次数为 n n n,也就说明每个状态都能到达这个密码,则答案加一。

#include 

using namespace std;
int n, m[10][6], ans;
map<string, int> mp;

int main(int argc, char const *argv[]) {
	cin >> n;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= 5; j++)
			cin >> m[i][j];
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= 5; j++) {
			for (int k = 0; k < 10; k++) {
				string s = "";
				for (int l = 1; l < j; l++) s += char(m[i][l] + '0');
				if (m[i][j] == k) continue;
				s += char(k + '0');
				for (int l = j + 1; l <= 5; l++) s += char(m[i][l] + '0');
				mp[s]++;
			}
			if (j == 5) continue;
			for (int k = 1; k < 10; k++) {
				string s = "";
				for (int l = 1; l < j; l++) s += char(m[i][l] + '0');
				s += char((m[i][j] + k + 10) % 10 + '0');
				s += char((m[i][j + 1] + k + 10) % 10 + '0');
				for (int l = j + 2; l <= 5; l++) s += char(m[i][l] + '0');
				mp[s]++;
			}
		}
	for (auto i : mp)
		if (i.second == n)
			ans++;
	cout << ans << endl;
}

T2 消消乐

这个题的重点在于如何判断一个串是否可以被消除:用一个栈依次读串里的字符,若该字符与栈顶的元素相同,则弹出栈顶,否则把该元素放入栈,最后看栈是否为空即可,这样就得到了 n 3 n^3 n3的做法,有 35 35 35分。

// 35pts
#include 
#define A 100010

using namespace std;
int n, top, ans; string s;
char sta[A];

int main(int argc, char const *argv[]) {
	cin >> n >> s;
	for (int l = 0; l < n; l++)
		for (int r = l + 1; r < n; r++) {
			top = 0;
			for (int i = l; i <= r; i++) {
				if (sta[top] == s[i]) top--;
				else sta[++top] = s[i];
			}
			if (top == 0) ans++;
		}
	cout << ans << endl;
}

在上面的做法中,对于相同的左端点,我们枚举了所有的右端点,重复判断了许多次,其实只要从左端点开始走一遍到末尾就可以,每走一步都判断一下栈是否为空。复杂度降为 n 2 n^2 n2,有 50 50 50分。

// 50pts
#include 
#define A 200010

using namespace std;
int n, top, ans; string s;
char sta[A];

int main(int argc, char const *argv[]) {
	cin >> n >> s;
	for (int l = 0; l < n; l++) {
		top = 0;
		for (int r = l; r < n; r++) {
			if (sta[top] == s[r]) top--;
			else sta[++top] = s[r];
			if (top == 0) ans++;
		}
	}
	cout << ans << endl;
}

再看下一个部分分,“字符串中的每个字符独立等概率地从字符集中选择”,也就是说我们确定了左端点之后,在右端点不断增大的过程中,这个串会越来越长,能产生消除串的概率会越来越低,所以给右端点定一个界限,我们认为在这个界限之后就不可能产生消除串。这个界限根据左端点的个数来定,该部分分的数据范围下 n ≤ 2 ∗ 1 0 5 n\leq2*10^5 n2105,那就卡着能跑过的数据范围,把右端点的界限设置到 1000 1000 1000 2 ∗ 1 0 8 2 ∗ 1 0 5 \frac{2*10^8}{2*10^5} 21052108=1000),这样能拿到 60 60 60分。

// 60pts
#include 
#define A 200010

using namespace std;
int n, top, ans; string s;
char sta[A];

int main(int argc, char const *argv[]) {
	cin >> n >> s;
	for (int l = 0; l < n; l++) {
		top = 0;
		int cnt = 1000;
		for (int r = l; r < n; r++) {
			if (sta[top] == s[r]) top--;
			else sta[++top] = s[r];
			if (top == 0) ans++;
			if (--cnt < 0 and n > 8000) break;
		}
	}
	cout << ans << endl;
}

其实能产生一个子串的条件是,这两个栈的状态是相同的。例如样例“accabccb”,初始时为空栈,代表的位置为 0 0 0;“acca”为空栈,代表的位置为 3 3 3;“accabccb”为空栈,代表的位置为 7 7 7;这三个位置中每两个位置的组合都可以产生一个串,也就是 C 3 2 C^2_3 C32,共可以产生 3 3 3个串。所以我们统计每种栈的状态出现的次数 n n n,答案累计上 C n 2 = n ( n − 1 ) 2 C^2_n=\frac{n(n-1)}{2} Cn2=2n(n1)即可。这样可以拿到 90 90 90分,会有两个点 M L E MLE MLE,就是刚才 50 − 60 50-60 5060分的两个点,原因是栈的状态太多,可能会产生大约 n n n个字符串,串的长度从 1 1 1递增到 n n n m a p map map占的空间太大。

// 90pts
#include 
#define A 2000010

using namespace std;
typedef long long ll;
int n, top;
string s, now;
char sta[A];
ll ans;
map<string, int> mp;

int main(int argc, char const *argv[]) {
	cin >> n >> s;
	mp[""]++;
	for (int i = 0; i < n; i++) {
		if (sta[top] == s[i]) top--, now.pop_back();
		else sta[++top] = s[i], now += s[i];
		mp[now]++;
	}
	for (auto i : mp)
		ans += 1ll * (i.second * 1ll * (i.second - 1ll) / 2);
	cout << ans << endl;
}

到这里可以想到把上面两个代码合一下就可以满分,所以现在问题来到了如何特判 11 、 12 11、12 1112这两个点。这两个点的条件上面也分析过了,“字符串中的每个字符独立等概率地从字符集中选择”, a − z a-z az 26 26 26个字母出现的概率是相同的,那我们就在输入字符串之后统计一下每个字母出现的次数,如果该字母出现的次数与期望次数的差值过大,那么我们认为这个点不是第 11 11 11个点或第 12 12 12个点。

// 100pts
#include 
#define A 2000010

using namespace std;
typedef long long ll;
int n, top, t[27];
string s, now;
char sta[A];
ll ans;
bool flag;
map<string, int> mp;

int main(int argc, char const *argv[]) {
	cin >> n >> s;
	for (int i = 0; i < n; i++) t[s[i] - 'a']++;
	for (int i = 0; i < 26; i++) // 判断测试点
		if (abs(t[i] - n / 26) > 10000) {
			flag = 1;
			break;
		}
	if (flag) {
		mp[""]++;
		for (int i = 0; i < n; i++) {
			if (sta[top] == s[i]) top--, now.pop_back();
			else sta[++top] = s[i], now += s[i];
			mp[now]++;
		}
		for (auto i : mp)
			ans += 1ll * (i.second * 1ll * (i.second - 1ll) / 2);
		cout << ans << endl;
	}
	else {
		for (int l = 0; l < n; l++) {
			top = 0;
			int cnt = 1000;
			for (int r = l; r < n; r++) {
				if (sta[top] == s[r]) top--;
				else sta[++top] = s[r];
				if (top == 0) ans++;
				if (--cnt < 0 and n > 8000) break;
			}
		}
		cout << ans << endl;
	}
}

你可能感兴趣的:(算法,c++,数据结构)