未来一段时间会在这个专栏下持续更新 2025“钉耙编程”中国大学生算法设计春季联赛 的相关题解,鉴于本人只是一个青名蒟蒻,很多题目可能都不会,但我会尽我所能的去理解。
1002 学历史导致的
题解:
暴力地从 1984 枚举到 2043,看一下每一年到底是天干地支中的哪两位就行了。
代码写的有点罗嗦了
void solve()
{
map<int, string> ha = {{0, "jia"}, {1 ,"yi"}, {2, "bing"}, {3, "ding"}, {4, "wu"},
{5, "ji"}, {6, "geng"}, {7, "xin"}, {8, "ren"}, {9, "gui"}};
map<int, string> ha2 = {{0, "zi"}, {1, "chou"}, {2, "yin"}, {3, "mao"}, {4, "chen"}, {5, "si"}, {6, "wu"}, {7, "wei"}, {8, "shen"}, {9, "you"}, {10, "xu"}, {11, "hai"}};
int n = 1984;
int m;
string s;
cin >> s;
string t, d;
int idx = 0;
for (int i = 1984; i <= 2043 ; i++, idx++) {
string ans = ha[idx % 10] + ha2[idx % 12];
if (ans == s) {
cout << i << endl;
return;
}
}
}
1003 学数数导致的
题解:
赛时样例都没看懂,一直以为是 ( 1 , 0 , 1 , 3 ) , ( 1 , 0 , 1 , 2 ) (1, 0, 1, 3), (1, 0, 1, 2) (1,0,1,3),(1,0,1,2) 和 ( 1 , 0 , 1 , 2 ) (1, 0, 1, 2) (1,0,1,2) 后来发现 后两组都是 ( 1 , 2 ) (1, 2) (1,2) 这样的数对,而题目没有说p,q不相等,浪费了点时间。
这里有一个很直观的思路:
从后向前遍历,统计出每个形如 (x, y) 的有多少对,而这就统计出了我们目标子序列的后半截,接下来只要遇到了0,我们就设置一个标记,表示之后只要遇到了x这个数字,那么就有相应的对数。
拿样例说明一下,走到倒数第四位为0的时候 总共有 ( 3 , 2 ) , ( 2 , 2 ) (3, 2), (2, 2) (3,2),(2,2) 两种组合,走到倒数第五位为1的时候就又多了 ( 1 , 3 ) , ( 1 , 2 ) (1, 3), (1, 2) (1,3),(1,2) 两种组合则在走到第二位2的时候,ans 直接加上以2开头的半截子序列的个数也就是1,走到第一位1的时候 ans 加上 2。
但是可能会有多个1,所以我们对每个数字的序列数取Max,然后最后再将所有的Max相加就行了。
void solve()
{
int n;
cin >> n;
vector<int> a(n + 1), sum(1e6 + 1, 0);
for (int i = 1; i <= n; i++) cin >> a[i], sum[a[i]]++;
vector<int> suf(1e6 + 1, 0), f = suf;
vector<int> num_0(1e6 + 1, 0), ans = num_0;
int sum_0 = 0;
int res = 0;
set<int> S;
for (int i = n; i >= 1; i--) {
if (a[i]) {
if (num_0[a[i]] < sum_0) {
ans[a[i]] = max(ans[a[i]], f[a[i]]);
num_0[a[i]] = sum_0;
}
f[a[i]] = S.size();
S.insert(a[i]);
} else {
sum_0 ++;
}
}
for (int i = 1; i <= 1e6; i++) {
res += ans[i];
}
cout << res << endl;
}
1004 学DP导致的
题解:
只要发现了长度最多不会超过 26 * |s| 这个性质后,这题就是一个板子题了,但是还要注意出题人的恶意,k只能用字符串读入,不然喜提一发罚时。
void solve()
{
string m, s;
cin >> s >> m;
int num;
if(m.size() >= 9) {
num = 26;
} else {
num = min(26, stoi(m));
}
string str = " ";
while(num--) {
str += s;
}
//cout << str << endl;
vector<int> f(str.size());
for (int i = 1; i < str.size(); i++) {
f[i] = 1;
for (int j = 1; j < i; j++) {
if (str[j] < str[i]) {
f[i] = max(f[j] + 1, f[i]);
}
}
}
int ans = *max_element(f.begin() + 1, f.end());
//cout << f.size() << endl;
cout << ans << endl;
}
1005 学几何导致的
题解:
这题只能说跟几何一毛钱关系都没有,完全就是一个跟周期相关找规律数学题。
而一般这种跟周期相关的题 第一步就是先打表找规律。
读完题,简单写几个数据我们就可以发现,如果k为奇数,那么就不可能有垂直的线段。
如果k 为偶数,那么一共也就会有k种不同角度的线段,然后mod 一下k,看最后还剩下多少个多余的线段可以分配,然后其实就可以定量的求出每一种线段上到底有多少根。
然后求和就行了。
void solve()
{
int n, k;
cin >> n >> k;
// 一共k种边
n--;
if (k & 1) {
cout << 0 << endl;
}
else {
int num = n / k, ans = 0;
mod = n % k;
int id = k / 2;
if (id > mod) {
ans += (mod + 1) * (num + 1) * num; // 1 2
ans += (k / 2 - 1 - mod) * num * num; // 1 1
}
else {
ans += (mod - id + 1) * (num + 1) * (num + 1); // 2 2
ans += (k - mod - 1) * (num + 1) * num; // 1 2
}
cout << ans << endl;
}
}
1006 学博弈论导致的
题解:
赛时没有推出来,最后推了个大概交了一发WA了,赛后才想明白应该怎么写:
首先明确两点:
- 游戏肯定会结束,输出“Tie”就是在骗人。
- 宝盒的存在与否根本不影响最后的结果。
这是一个公平组合游戏,最后就一定会有人输,有人赢 其实我是看样例没有给Tie才这么猜的
第二点,无论先手后手,只要人有选择了关于盒子的操作试图扭转情况,那么另一个人一定会在下一回合中有一种策略消除宝盒产生的石子的影响,所以宝盒不会影响最后的结果(自己一推就知道)。
至此,我们已经把这个问题简化为了只有前四种操作,两种不同石子的博弈论模型。
-
先看第一条 “拿走 k k k 块红宝石 ( 1 ≤ k ≤ 3 ) (1≤k≤3) (1≤k≤3)” 很难让人不联想到巴什博弈。根据巴什博弈的结论,当r等于 (k + 1) 的倍数也就是 4 的时候,Alice必输,具体证明看我另一篇博客。
-
如果我们假设 b = 0,那么就会发现上述的结论依旧成立,但是这道题当中b的数量不一定等于 0,所以我们还得再思考一下b的数量所能带来的影响。
-
经过打表,我们可以发现,当 b = 1 时,Alice必胜:
-
可以感性地理解为当 b 多了 1 后,最后的操作数就一定会多1次,譬如当Alice先拿走这一块 b,那么 (4, 0) 的必败局面就会留给Bob,再譬如Alice先拿走一块 r,那么 (3, 1) 这种必败局面也会留给Bob。
-
同理:(5, 1) 的情况也是如此。
-
但是 (6, 1) 的情况就变了,不管Alice怎么操作 (5, 1),(4, 1),(3, 1),(5, 0) 这些局面都是必胜的,也就是Bob必胜。
-
我们再来思考偶数 (4, 2) ,我们发现此时 Alice 必败:
-
Alice先手,不管进行哪种操作,Bob都会进行相应的操作导致Alice必输,大家可以推一下这三种操作。
至此我们第一步打表分析就结束了,最后猜出了一个可能不太靠谱的结论,就是Alice的胜负确实跟 r 是不是4的倍数有关系,但同时还和 b 的奇偶性有一定的关系。
我们可以再来思考一下:
假设 Alice 必输,假设 r 是 4 的倍数,而且 b 是偶数,Alice每次操作所产生的影响都可以被Bob的下次操作反过来。
- Alice 拿了 m 块 r 石子,那么 Bob就可以拿 (k + 1 - m) 块 r 石子保证最后 r 还是4的倍数。
- Alice 拿了一块 b,并且 r 不减,那么 Bob 也可以拿一块 b 让 b 保持奇偶性相同。
- ALice 拿了一块 b,而且拿了一块 r,那么Bob 就拿一块 r, 下一回合中
Alice拿了 1 块 r,Bob 拿一块 r 和 一块 b,此时恢复。
Alice拿了 2 块 r,Bob 只拿一块 b, 此时恢复。
Alice拿了 3 块 r , Bob 把一块 蓝宝石 变为 红宝石,此时恢复。
Alice拿了 1 块 b …
在这些所有的可能性中,我们发现,鉴于 k 是 3,而且关于 b 的操作都可以做到 +1 r, -1 r, r 不变,使得一回合(或者两回合)下来,总能保证原有的 r 是 4 的倍数,b 是偶数的性质不变。
至此我们就找到了一个周期。
而事实上,我们在刚才的模拟中又发现了另一个周期,也就是当 r % k = 2,b 为 奇数 的情况,可以通过一回合操作将这种情况变为 r 是4
的倍数,而且 b 是偶数的情况。
回顾一下我们的做题过程:打表 --> 猜结论 --> 尝试数学证明或模拟
所以 正确代码只有一行:
void solve()
{
int r, b, m;
cin >> r >> b >> m;
if (((r % 4 == 0) && (b & 1) == 0) || ((r % 4 == 2) && (b & 1) == 1)) {
puts("Bob");
} else {
puts("Alice");
}
}
1008 学画画导致的
题解:
看人家代码才有的思路,原来是用拓扑排序。
颜色一层叠一层就符合某种拓扑序,然后就联想到了拓扑图(果然写这种题还是太少了,完全没这种意识)
至于怎么建边:
首先我们都知道每一个小的三角形都至少是被三种颜色所覆盖的,所以一旦这个小三角形呈现出最终的一种颜色1,那么颜色2和颜色3一定就会指向颜色1,拓扑排序时,颜色1就会在颜色2和颜色3的后面,并且颜色1入度+2.
所以接下来的代码就很简单了,先获取每个点的三种颜色,从两个没有显色的颜色向已显色的颜色连边,然后拓扑排序,一旦产生了自环,那就失败了。
void solve()
{
int n, m;
cin >> n >> m;
vector<vector<int>> e(n * 3 + 1);
vector<int> ind(n * 3 + 1, 0);
int ans = 1;
while(m--) {
int x, y, col;
cin >> x >> y >> col;
auto A = [&](int x, int y) -> int { return (y + 1) >> 1; };
auto B = [&](int x, int y) -> int { return (n - x + 1) + n; };
auto C = [&](int x, int y) -> int { return (x - y / 2) + n * 2; };
int a = A(x, y), b = B(x, y), c = C(x, y);
if (col == a) {
e[b].push_back(a);
e[c].push_back(a);
} else if (col == b) {
e[a].push_back(b);
e[c].push_back(b);
} else if (col == c) {
e[a].push_back(c);
e[b].push_back(c);
} else {
ans = 0;
}
ind[col] += 2;
}
if (ans == 0) {
cout << "No" << endl;
return ;
}
auto topsort = [&]() -> void {
queue<int> q;
for (int i = 1; i <= 3 * n; i++) {
if (ind[i] == 0) {
q.push(i);
}
}
vector<int> res;
while (!q.empty())
{
int t = q.front();
q.pop();
res.push_back(t);
for (auto v : e[t]) {
if (--ind[v] == 0) {
q.push(v);
}
}
}
if (res.size() == 3 * n) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
};
topsort();
}