CSUACM2024新生赛 - 第3场 题解
写在前面
比赛地址:https://www.luogu.com.cn/contest/217790
A 袋鼠的迷宫
对于所有相邻的两列,如果两行都存在障碍物,则无法继续前进,此时判断为无法到达即可。
#include <iostream>
#include <string>
using namespace std;
const int N = 1e6 + 5;
const string ans[2] = {"NO", "YES"};
int n;
bool vis[2][N];
int main() {
cin >> n;
for(bool j : {0, 1})
for (int i = 1; i <= n; i++) {
char c;
cin >> c;
vis[j][i] = (c == '.');
}
bool flag = 1;
for (int i = 2; i <= n; i++)
flag &= ((vis[0][i - 1] && vis[0][i]) || (vis[1][i - 1] && vis[1][i]));
cout << ans[flag] << endl;
return 0;
}
B 袋鼠大元帅
首先发现我们可以把所有袋鼠用 \(2(n - 1)\) 次操作聚集到任一个角。所以我们选择距 \((a,b)\) 最近的角,讨论 \(n\) 的奇偶,可发现最多还需 \((n - 1)\) 次操作。
#include <iostream>
using namespace std;
int main() {
int n, a, b;
cin >> n >> a >> b;
if(a*2 <= n) {
for (int i = 1; i < n; i++)
cout << 'U';
for (int i = 1; i < a; i++)
cout << 'D';
}
else {
for (int i = 1; i < n; i++)
cout << 'D';
for (int i = n; i > a; i--)
cout << 'U';
}
if(b*2 <= n) {
for (int i = 1; i < n; i++)
cout << 'L';
for (int i = 1; i < b; i++)
cout << 'R';
}
else {
for (int i = 1; i < n; i++)
cout << 'R';
for (int i = n; i > b; i--)
cout << 'L';
}
return 0;
}
C 袋鼠排队
首先考虑暴力模拟,依次向每个序列末尾添加,如果遇到 \(-1\) 就表示当前这个序列已经填完,之后再填到这个序列就跳过。
考虑复杂度,若某个序列特别长,在其他序列都填完后每次再填入一个数需要遍历每个序列,复杂度 \(O(m^2)\)。
但注意到每次遍历已填完的序列很浪费,所以我们使用链表,一个序列填完后就把他删除,复杂度 \(O(m)\) 。
需使用vector存储每个序列,防止mle。
#include <iostream>
#include <vector>
using namespace std;
const int N = 3e5 + 5;
int n, m, b[N], nxt[N], lst[N];
vector<int> a[N];
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> b[i];
n += (b[i] == -1);
}
for (int i = 1; i <= n; i++)
nxt[i] = i % n + 1, lst[i] = i - 1;
lst[1] = n;
for (int i = 1, j = 1; i <= m; i++, j = nxt[j]) {
if(b[i] != -1)
a[j].push_back(b[i]);
else
lst[nxt[j]] = lst[j], nxt[lst[j]] = nxt[j];
}
cout << n << endl;
for (int i = 1; i <= n; i++) {
cout << a[i].size() << ' ';
for(int j = 0; j < a[i].size(); j++)
cout << a[i][j] << ' ';
cout << endl;
}
return 0;
}
D 袋鼠总统
二分答案加树形 dp。
答案显然具有单调性(若某个 \(x\) 能满足预算,则比他大的 \(x\) 都能满足预算),考虑二分答案。
考虑如何检查某个 \(x\) 是否合法。设 \(f(u)\) 表示若要满足 \(u\) 子树内的预算至少还需要从祖先那里获得多少补助,所以转移时有:
复杂度 \(O(n\log a)\)。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 5, A = 1e9;
int n, a[N], head[N], nxt[N], to[N], edge_cnt;
inline void add_edge(int u, int v) {
nxt[++edge_cnt] = head[u];
head[u] = edge_cnt;
to[edge_cnt] = v;
}
ll dfs(int u, int x) {
ll res = a[u] - x;
for (int i = head[u]; i; i = nxt[i])
res += dfs(to[i], x);
return max(res, 0ll);
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for(int i = 2; i <= n; i++) {
int p;
cin >> p;
add_edge(p, i);
}
int l = 0, r = A;
while(l < r) {
int mid = (l + r) / 2;
if(dfs(1, mid))
l = mid + 1;
else
r = mid;
}
cout << l << endl;
return 0;
}
E 袋鼠喝奶茶
贪心,细节题
因为每次都会删去整段前缀,所以可以把连续的 01 子串看为一段。显然一次操作减少的段数只能为 1 或 2,则一种显然的贪心策略是,尽可能每次操作仅减少一段。
设段长为 \(\operatorname{len}\),则最优情况下,每次操作应当选择 \(\operatorname{len}> 1\) 的、最靠左的段删除,并删除其中一个字符,即可保证只会减少一段;若没有 \(\operatorname{len}>1\) 的段,则每次删除就会减少两端,但是由于只进行一步操作也算一次,所以要记得上取整。
思路比较好想,细节可以看代码。
#include <iostream>
#include <vector>
using namespace std;
void solve() {
int n; cin >> n;
string s; cin >> s;
s = '1' + s;
vector<int> cnt(n + 1, 0); //统计每一段的长度
int nw = 0; //统计总段数
for (int i = 1; i <= n; ) {
char ch = s[i];
int j = i + 1;
while(j <= n && s[j] == ch) j++;
cnt[++nw] = j - i;
i = j;
}
int j = 1, x = -1; //删除操作具有单调性,用j表示删到哪里,x判断有多少不可删
for (int i = 1; i <= nw; i++) {
if (j < i) j = i; //只能删除当前段或者后面的
while (j <= nw && cnt[j] <= 1) j++;
if (j >= nw + 1) {x = i - 1; break;} //没有可以删的就break直接计算
cnt[j]--;
}
if (x != -1) cout << x + (nw - x + 1) / 2 << endl;
else cout << nw << endl; //最多也只能删分段的次数
}
int main() {
int t;
cin >> t;
while (t--)
solve();
return 0;
}
F 袋鼠逛商场1
gxd出的,灵感来源是ICPC Taichung Regional 的 H, 出了就没指望有人做出来(笑)
若不限制初始位于第一层,则本题为:限制条件为连续的 +-
不能超过 \(k-1\) 个,求构造长为 \(n\) 的序列的方案数。显然此时 +-
是等价的,则考虑用 0 表示操作 0,1 表示操作 +-
。考虑 DP,设 \(f[i][j]\) 表示,只考虑前 \(i\) 个位置,第 \(i\) 个位置放操作 \(j\) 时的合法方案数,则:
- 0 没有限制可以直接转移:
- 为了使当前位置合法,连续 1 不超过 \(k-1\) 个,可以枚举非 1 的断点 \(p\) 求和,并通过前缀和 \(pre\) 实现 \(O(1)\) 转移:
注意特判处理 \(i-k-1<0\) 的情况。
最后处理从第一层开始的限制,把序列翻转过来考虑,题意变为从任意层开始,最后必须落在第一层,因为第一层不能下行所以答案为 \(f[n][1]+f[n][0]\)
第一维可以滚动优化。这时发现维护前缀和即可, 设 \(f[i][j]\) 的前缀和为 \(s[i][j]\),可进一步优化转移。又发现 +-
等价,代码中 \(f[1]\) 表示的就是填 +-
的个数,所以转移时 \(\times 2\)。
#include <iostream>
using namespace std;
const int N = 1e6 + 5, mod = 998244353;
int n, k, s[N];
inline int add(int x, int y) {
int res = x + y;
return res >= mod ? res - mod : res;
}
inline int sub(int x, int y) {
int res = x - y;
return res < 0 ? res + mod : res;
}
int main() {
cin >> n >> k;
int f[2] = {1, 0};
s[0] = 1;
for (int i = 1; i <= n; i++) {
f[0] = add(f[0], 2*f[1]%mod);
f[1] = sub(s[i - 1], i - k < 0 ? 0 : s[i - k]);
s[i] = add(s[i - 1], add(f[0], f[1]));
}
cout << add(f[0], f[1]) << endl;
return 0;
}
G 袋鼠逛商场2
DP, 乘法逆元
请先阅读前一题题解。
和前一题的区别在于0失去了断点功能,而+-互为断点
dp转移式变为:
枚举 \(n\) 个数中的 +-
总个数,用 0 填充剩余部分,则每个方案对于答案的贡献为 \(f[i]\times C(n-i, n)\)。
特殊限制等处理方式同前。
#include <iostream>
using namespace std;
const int N = 1e6 + 5, mod = 998244353;
int n, k, s[N];
inline int add(int x, int y) {
int res = x + y;
return res >= mod ? res - mod : res;
}
inline int sub(int x, int y) {
int res = x - y;
return res < 0 ? res + mod : res;
}
inline int mul(int x, int y) {
return 1ll * x * y % mod;
}
int qpow(int x, int y) {
int res = 1;
while(y) {
if(y&1)
res = mul(res, x);
x = mul(x, x);
y >>= 1;
}
return res;
}
int main() {
cin >> n >> k;
s[0] = 1;
int res = 1;
for (int i = 1, c = 1; i <= n; i++) {
int f = sub(s[i - 1], i - k < 0 ? 0 : s[i - k]);
c = mul(c, mul(qpow(i, mod - 2), n + 1 - i));
res = add(res, mul(f, c));
s[i] = add(s[i - 1], f);
}
cout << res << endl;
return 0;
}
写在最后
如果澳大利亚的袋鼠集结入侵乌拉圭,那么最后获得胜利的是哪一方? - 知乎:
https://www.zhihu.com/question/320455456。