2023-2024 ACM-ICPC Latin American Regional Programming Contest
2023-2024 ACM-ICPC Latin American Regional Programming Contest
C. Candy Rush
题意
给你一个长度为 \(n\) 的数组 \(C\),以及一个种类数量 \(k\),其中 \(1\leq C_i\leq k\),现在你需要找到一个最长的连续子序列,满足
序列每种数出现的次数都一样。
做法
首先我们可以类比 \(k\) 比较小的情况,例如 \(k=2\),那么我们可以设所有满足 \(C_i=1\) 的值为 \(1\),满足 \(C_i=2\) 的值为 \(-1\),
那么题目就 \(\iff\) 求最长的连续子序列,满足 \(\sum_{i=l}^rA_i=0\)。
这个显然可以用哈希表 \(+\) 前缀和快速搞定。
那么当 \(k\) 很大的时候,我们应该怎么办,我们也想能不能给这每个种类都设一个值。
\(C_i=1\rightarrow v_1\)
\(C_i=2\rightarrow v_2\)
\(\dots\)
\(C_i=k\rightarrow v_k\)
我们令 \(v_k=-v_1-v_2-...v_{k-1}\)
那么当区间 \([l,r]\) 出现的所有种类出现次数都一样的时候,一定有 \(\sum_{i=l}^rv_i=0\) 。
当此时这个条件不是充分的,它只是必要的。
这里我们需要引入一个随机化的技巧,我们通过给每一个种类随机一个比较大的值,就可以让我们这个条件在很多情况下都变成充要的。(由于随机的数值范围很大,相当于犯错的概率是很低的)
我们可以利用 \(mt19937\) 给这些数随机一个比较大的值,任何就可以套用我们 \(k=2\) 的做法了。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 4e5 + 10;
LL c[N];
mt19937 mrand(random_device{}());
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
for (int i = 1; i < k; i++) {
c[i] = mrand() % 1000000000 + 1;
}
c[k] = 0;
for (int i = 1; i < k; i++) {
c[k] -= c[i];
}
map<LL, int> mp;
int ans = 0;
LL pre = 0;
mp[pre] = 0;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
pre += c[x];
if (mp.count(pre)) {
ans = max(ans, i - mp[pre]);
} else mp[pre] = i;
}
cout << ans << "\n";
return 0;
}
J. Journey of the Robber
题意
给你 \(n\) 个结点的树,每个点有个权值,\(1\) 号点的权值为 \(1\),\(2\) 号点权值为 \(2\),以此类推。
现在你需要对于每一个点 \(u\),都要求出一个点 \(v\),满足 \(d(u,v)\) 是最小的,且 \(v\) 的权值要大于 \(u\) 的权值,即 \(v>u\)。
做法
首先考虑树形 DP 能不能做,显然是不太能做的,换根之后我们并不好维护;在考虑 DSU on tree 好像也不好做,我们需要更新每一个点,但树上启发式合并,我们是不能暴力遍历重儿子的,所以我们无法直接更新重儿子的信息。
那么对于这种和路径相关的,我们就只剩下点分治了,那么我们每次找到重心 \(mid\),相当于问题变成对于每一个点都查询领域,问题显然可以分成经过重心或者不经过,不经过重心的我们直接递归分治即可。经过重心,我们只要把所有点维护到重心的距离,然后按权值排序,直接暴力遍历更新和重心连接的这些子树内所有点的答案,排序之后维护后缀最小值即可。
这里有个值得思考的,就是我们直接全体排序,是会查询到某个点不经过重心的路径,但我们发现这样查询的值是会偏大的,故不会影响真正和 \(u\) 在同一颗子树,不经过重心的路径的答案,所以可以这样做,但如果要求数量,我们就需要特别注意了。
Code
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;
vector<int> g[N];
int sz[N], maxs[N], d[N];
bool del[N];
int n;
PII ans[N];
void solve(int u, int s) {
int ms = s + 1, root = -1;
function<void(int, int)> self = [&](int u, int par) {
sz[u] = 1;
maxs[u] = 0;
for (auto v : g[u]) {
if (del[v] || v == par) continue;
self(v, u);
sz[u] += sz[v];
maxs[u] = max(maxs[u], sz[v]);
}
maxs[u] = max(maxs[u], s - sz[u]);
if (maxs[u] < ms) ms = maxs[u], root = u;
};
self(u, -1);
d[root] = 0;
vector<PII> cur;
cur.push_back(make_pair(root, 0));
for (auto v : g[root]) {
if (del[v]) continue;
d[v] = 1;
function<void(int, int)> dfs = [&](int u, int par) {
sz[u] = 1;
cur.push_back(make_pair(u, d[u]));
for (auto v : g[u]) {
if (del[v] || v == par) continue;
d[v] = d[u] + 1;
dfs(v, u);
sz[u] += sz[v];
}
};
dfs(v, root);
}
sort(cur.begin(), cur.end());
reverse(cur.begin(), cur.end());
PII res = make_pair(n, n);
for (auto [x, y] : cur) {
ans[x] = min(ans[x], make_pair(d[x] + res.first, res.second));
res = min(res, make_pair(y, x));
}
//分治下去
del[root] = true;
for (auto v : g[root]) {
if (!del[v]) solve(v, sz[v]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++) ans[i] = make_pair(n, n);
solve(1, n);
for (int i = 1; i <= n; i++) {
cout << ans[i].second << " \n"[i == n];
}
return 0;
}
K. Keen on Order
题意
给你一个长度为 \(n\) 的数组 \(V\),和一个 \(k\),其中数组每个元素满足 \(1\leq V_i \leq k\)。
现在问你存不存在一个长度为 \(k\) 的排列 \(P\),使得 \(P\) 不是数组 \(n\) 的任何一个子序列。
如果存在,请你输出任意一个满足要求的一个排列,否则请你输出 *
。
数据范围
\(1\leq n, k\leq 300\)。
做法
首先有一个比较明显的贪心做法,在基于子序列的匹配过程,我们可以想到一个如下的贪心手段:
-
我们假设现在我们整个排列这个子序列匹配到序列中的位置是 \(last\),现在我们需要看看第 \(i\) 位填啥。
-
如果对于所有没填过的数 \(v\),在 \(last\) 后面找不到,那么显然我们这一位填 \(v\),就可以造成匹配结束。
-
否则,我们设 \(pos_v\) 表示值为 \(v\) 在 \(last\) 后面第一次出现的位置,我们只要尽可能填 \(pos_v\) 尽可能大的值。
这样看上去是很对的,但是我们不能证明这个做法是充要的,意思是我们这种方法有可能找不到一组解。
例如下面的这个例子:
当 \(n=10,k=4\),数组 \(V\) 为:\(1,3,2,4,1,3,2,1,3,1\)
显然按照我们的方法,我们会先匹配到 \(4,2,3,1\),然后就无解了。
但我们发现其实 \(2,1,4,3\) 这个排列是一组合法的解。
此时又需要分析了,这也是比较难的部分,当我们难以下手的时候,我们可以尝试数据分治,我们想一下
-
当 \(k\) 比较小的时候,我们可以怎么做,我们显然可以暴力状压 DP,我们设 \(dp[i]\) 表示目前已经用的数为 \(i\),
且按贪心的子序列匹配方法,匹配到的最后一个位置的最大值是多少。
然后转移 \(dp[mask]=\max_{x\in mask}(next(x,dp[mask-x])\)。
其中 \(next(i,j)\) 表示值为 \(i\) 在 \(j\) 后面第一次出现的位置是多少。
如果最后转移完 \(dp[(1<<k)-1]>n\),那么说明我们找到了一组合法的排列,倒推回去方案即可。
-
当 \(k\) 比较大的时候,我们怎么办,我们就思考我们上面的哪个不太对的贪心做法,我们发现我们假设当且匹配到位置为
\(pos\),我们第一次填数,至少都会移动 \(k\) 个单位,否则我们就找到一组解了,那么以此类推,我们 \(pos\) 每次至少会移动:\(k+k-1+k-2+...+1\),我们发现当 \(k\ge 25\) 的时候,这个求和式子就 \(>300\),显然是会找到一组解的。
所以我们就得到了数据分治的做法,当 \(k\) 较大的时候,我们就采用上面的贪心做法,较小的时候就暴力状压 DP 即可。
Code
#include <bits/stdc++.h>
using namespace std;
int dp[1 << 24];
pair<int, int> last[1 << 24];
int nxt[310][26], st[26];
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
{
vector<int> b(k + 1);
vector<bool> st(k + 1, false);
int last = 0;
for (int i = 1; i <= k; i++) {
vector<int> suf(k + 1, 0);
for (int j = last + 1; j <= n; j++) {
if (!suf[a[j]]) suf[a[j]] = j;
}
int pos = 0, ms = 0;
for (int j = 1; j <= k; j++) {
if (!st[j] && !suf[j]) {
//说明此时填 j 已经起飞了
b[i++] = j;
st[j] = true;
for (int p = 1; p <= k; p++) {
if (!st[p]) b[i++] = p;
}
for(int p = 1; p <= k; p++) {
cout << b[p] << " \n"[p == k];
}
return 0;
} else if (!st[j]) {
if (ms < suf[j]) ms = suf[j], pos = j;
}
}
b[i] = pos, st[pos] = true;
last = ms;
}
}
for (int i = 1; i <= k; i++) st[i] = n + 1, nxt[n + 1][i] = n + 1;
for (int i = n; i >= 0; i--) {
for (int j = 1; j <= k; j++) {
nxt[i][j] = st[j];
}
st[a[i]] = i;
}
for (int i = 1; i < 1 << k; i++) {
for (int j = 0; j < k; j++) {
if (i >> j & 1) {
if (dp[i] < nxt[dp[i - (1 << j)]][j + 1]) {
dp[i] = nxt[dp[i - (1 << j)]][j + 1];
last[i] = {i - (1 << j), j + 1};
}
}
}
}
int state = (1 << k) - 1;
if (dp[state] != n + 1) {
cout << "*\n";
} else {
vector<int> b;
while (state) {
auto u = last[state];
b.push_back(u.second);
state = u.first;
}
for (int i = k - 1; i >= 0; i--) {
cout << b[i] << " \n"[i == 0];
}
}
return 0;
}