USACO2022JAN G
因为某种神秘力量(懒)我错过了这场比赛,就补一篇题解吧。难度(自认为)从小到大排列。
[USACO22JAN] Farm Updates G
给出一张 \(n\) 个点的无向图,刚开始所有点的权值都是 \(1\),没有边。共有 \(q\) 次操作,操作分以下三种:
R x
将一个点的权值变为 \(0\)。A x y
在两点 \(x,y\) 之间连一条边,保证 \(x,y\) 的权值均为 \(1\)。D e
删除加入的第 \(e\) 条边,边从 \(1\) 开始编号。如果一个点权值为 \(1\) 或与权值为 \(1\) 的点连通,则定义这个点是「关联的」。对于每个点求最大的 \(i(0\le i\le q)\),使得在这次操作后该点是「关联的」。(\(1\le n\le 10^5,1\le q\le 2\times10^5\))
分析一波这些操作,我们能发现第二个操作似乎并不会改变每个点的关联性。注意到一个连通块内的点关联性都是相同的,而二操作相当于合并两个权值均为 \(1\) 的连通块,当然不会改变连通性。所以现在我们要维护的东西就变为了单点修改和删边。显然加边更好处理,所以考虑改成倒序处理。
我们来分析一下倒序的这些操作。
- 将一个点的权值从 \(0\) 变为 \(1\)。此时该点所在的连通块都会变为「关联的」。
- 删除 \(x,y\) 之间的边,保证 \(x,y\) 的权值均为 \(1\)。(注意这里依然有这个保证是因为正向做的时候就不会影响 \(0,1\))显然不会对关联性造成什么影响,可以忽略。
- 加入第 \(e\) 条边。这个操作影响关联性的条件是这条边两边的连通块关联性不同,合并之后就会全变为「关联的」。其他情况就没有影响,直接合并连通块即可。
想一下我们现在的操作,遍历一个连通块,合并两个连通块,而这些一个并查集就可以做到。而倒序操作从非「关联的」变为「关联的」的时间点的上一个时间,就是最终的答案。(注意一个点一旦变为「关联的」就不会再变回去了,因为上面的操作并不会影响到这一点)
实现时不要忘了倒序操作前加入在最后没有删除的边。时间复杂度是均摊的 \(\mathcal{O}(n+q)\)。
#include <cstdio>
#include <vector>
#include <cstdlib>
const int N = 2e5 + 10; int f[N]; std::vector<int> vec[N];
struct query{ int op, x; }q[N]; int ans[N], vis[N];
struct edge{ int x, y; }e[N]; int tp, del[N];
int getf(int x) { return x == f[x] ? x : f[x] = getf(f[x]); }
inline void link(int u, int v, int now)
{
u = getf(u), v = getf(v); if (u == v) return ;
if ((!vis[u]) ^ (!vis[v]))
{
if (!vis[u]) for (auto x : vec[u]) vis[x] = now;
else for (auto x : vec[v]) vis[x] = now;
}
if (vec[u].size() > vec[v].size()) { f[v] = u; for (auto x : vec[v]) vec[u].push_back(x); }
else { f[u] = v; for (auto x : vec[u]) vec[v].push_back(x); }
}
int main()
{
char op[5]; int x, y, n, Q; scanf("%d%d", &n, &Q);
for (int i = 1; i <= n; ++i) f[i] = i, vis[i] = Q, vec[i].push_back(i);
for (int i = 1; i <= Q; ++i)
{
scanf("%s", op);
if (op[0] == 'A') scanf("%d%d", &x, &y), e[++tp].x = x, e[tp].y = y;
else scanf("%d", &x), q[i].op = op[0] == 'D' ? 1 : 2, q[i].x = x;
}
for (int i = 1; i <= Q; ++i)
if (q[i].op == 1) vis[q[i].x] = 0;
else del[q[i].x] = 1;
for (int i = 1; i <= tp; ++i) if (!del[i]) link(e[i].x, e[i].y, Q);
for (int i = Q; i >= 1; --i)
{
if (!q[i].op) continue;
if (q[i].op == 1)
{
int u = getf(q[i].x);
if (!vis[u]) for (auto x : vec[u]) vis[x] = i - 1;
}
else link(e[q[i].x].x, e[q[i].x].y, i - 1);
}
for (int i = 1; i <= n; ++i) printf("%d\n", vis[i]);
return 0;
}
[USACO22JAN] Drought G
给出一个长为 \(n\) 的序列 \(h\),求出有多少个长为 \(a\) 的序列满足以下条件:
- \(0\le a_i\le h_i\)。
- 能通过每次选择一个 \(i(1\le i<n)\),把 \(a_i,a_{i+1}\) 同时减去 \(1\),进行若干次来使整个序列变成同一个非负整数。
答案对 \(10^9+7\) 取模。(\(1\le n\le100,1\le h_i\le10^3\))
这种题一看不像什么推式子的题,就可以考虑计数 \(\rm dp\) 了。考虑设 \(f_{i,j,k}(j\ge k)\) 表示对于前 \(i\) 个数,第 \(i\) 个数最后会变为 \(j\),其他的数最后会变为 \(k\) 的方案数。则注意到我们在转移时动 \(k\) 并不是一个明智的选择,毕竟这牵扯到的数太多了,所以如果要从 \(f_{i,j,k}\) 转移到 \(f_{i+1,x,k}\),我们的目标应该是把第 \(i\) 个数从 \(j\) 变为 \(k\)。即要对 \(i\) 操作恰好 \(j-k\) 次,这样的话,第 \(i+1\) 个数最初的值应该是 \(x+j-k\),由于有条件 \(1\) 的限制,所以我们有转移时 \(x\) 的取值范围:
所以把 \(\rm dp\) 方程写出来就是:
随便做个前缀和就可以做到 \(\mathcal{O}(nh_i^2)\) 的转移了。
接下来就是答案统计的问题了,显然最后我们要所有的数都一样,看起来答案应该是:
在 \(n\) 是奇数的时候,这样统计确实没错。但当 \(n\) 是偶数的时候,我们是能够通过题目中所给的操作给全部的数减去某个非负整数的,所以本质上所有 \(f_{n,k,k}\) 都被 \(f_{n,0,0}\) 包含,此时答案即为 \(f_{n,0,0}\)。
这样直接实现可能有点麻烦,这里参考了 USACO 官方的实现方法。考虑我们刚刚关心的只是 \(j,k\) 的差,所以我们新设一个状态 \(f_{i,l}\) 表示前 \(i\) 个,\(j-k=l\) 的方案数。这样把第一维滚动掉后,实现就非常简洁了。我们需要对每个 \(k\) 都做一次,注意 \(0\le k\le \min\{h_i\}\),每次做的时候只需要令 \(h_i\) 为 \(h_i-k\) 就可以方便实现边界赋值和转移条件。时间复杂度依然是 \(\mathcal{O}(nh_i^2)\),但空间省掉不少。
#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 110, M = 1100, mod = 1e9 + 7; int f[M], pre[M], h[N];
int main()
{
int n, mn = M, ans = 0; scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &h[i]), mn = std::min(mn, h[i]);
do
{
for (int i = 0; i <= h[1]; ++i) pre[i] = i + 1;
for (int i = h[1] + 1; i <= 1000; ++i) pre[i] = h[1] + 1;
for (int i = 2; i <= n; ++i)
{
memset(f, 0, sizeof (f));
for (int j = 0; j <= h[i]; ++j) f[j] = pre[h[i] - j];
pre[0] = f[0];
for (int j = 1; j <= 1000; ++j) pre[j] = (pre[j - 1] + f[j]) % mod;
}
(ans += pre[0]) %= mod;
for (int i = 1; i <= n; ++i) --h[i];
}while ((n & 1) && mn--);
printf("%d\n", ans); return 0;
}
[USACO22JAN] Tests for Haybales G
有一个长为 \(n\) 的递增的整数数组满足 \(x_1\le x_2\le \cdots\le x_n\) 和一个整数 \(k\)。你不知道这个数组 \(x\) 和这个整数 \(k\),但你知道对于每个下标 \(i\) 满足 \(x_{j_i}\le x_i+k\) 的最大下标 \(j_i\)。保证 \(i\le j_i\),且 \(j_1\le j_2\le \cdots\le j_n\le n\)。通过 \(j\) 数组,构造一个任意数组 \(x\) 和整数 \(k\) 满足上述条件,需要满足 \(1\le x_i,k\le 10^{18}\),可以证明答案一定存在。(\(1\le n\le 10^5\))
神仙构造题。考虑把题目中的关系描述为树形的关系,具体来讲是连 \(j_i+1\rightarrow i\) 的边,这样能形成一棵外向树,其中父子关系 \(u\rightarrow v\) 表示 \(u\) 是第一个满足 \(x_u>x_v+k\) 的下标。显然这样会多出一个虚节点 \(n+1\),它就是根,且不管怎么样都可以防止形成森林。
接下来我们发现对于树上同一层的结点,我们关心的是它们之间比较“微小”的数量变化,而对于不同层的结点我们关心的是“较大”的数量变化。能发现,这里大小的定义就是与 \(k\) 的关系,父亲结点和儿子结点之间的差“大约”是 \(k\),而同层只需要考虑递增的条件微调一下即可。
有了这个发现,我们就可以定义每个结点的权值是 \(h_ik+y_i(0\le x_i<k)\),其中 \(h_i\) 是该点距离最深的叶子结点的距离,可以形象理解为高度。我们发现这个权值完全符合刚刚的分析,不同层之间差的大约是 \(k\),还有 \(y_i\) 之间的小小调整,而同层之间只有 \(y_i\) 之间的小小调整。现在的目标就是恰当的给 \(y_i\) 赋值了。
给 \(y_i\) 赋值的限制在于满足原题递增的限制,因为一旦数组递增,其他的限制都能被满足。我们发现,如果从根节点开始 \(\rm dfs\),每次访问子节点都按照编号从大到访问,满足 \(y_i\) 按照访问次序递减,就能满足条件。首先因为 \(h_ik\),高层一定大于低层,而又因为 \(i\le j_i\),高层编号一定大于低层编号,所以大体上的递增是满足的。而每次我们又是按照编号从大到小访问子节点的,同层的又能保证递增(容易发现不同的父亲结点对应的儿子集合的编号是个区间且不会相交,且是按照父亲结点的大小排列的),所以整体满足递增。
至于 \(k\) 的选择,因为 \(y_i\) 要 \(n\) 个结点人手一个,还要保证单调递减(这是因为父亲结点和儿子结点之间的关系是 \(<\),所以 \(y_i\) 也要单调递减),所以 \(k\) 选 \(n+1\) 就好,每次 \(-1\)。好了,分析完这么多,实现一遍 \(\rm dfs\) 就能搞定,时间复杂度 \(\mathcal{O}(n)\)。
#include <cstdio>
#include <vector>
#include <algorithm>
const int N = 1e5 + 10; std::vector<int> T[N]; int dep[N], x[N], now, mx;
void dfs(int u, int depth)
{
dep[u] = depth; x[u] = now--;
for (auto v : T[u]) dfs(v, depth + 1);
}
int main()
{
int n; scanf("%d", &n);
for (int i = 1, j; i <= n; ++i) scanf("%d", &j), T[j + 1].push_back(i);
for (int i = 1; i <= n + 1; ++i) std::sort(T[i].begin(), T[i].end(), [](const int& a, const int& b) { return a > b; });
now = n + 1; dfs(n + 1, 0); for (int i = 1; i <= n + 1; ++i) mx = std::max(mx, dep[i]);
printf("%d\n", n + 1);
for (int i = 1; i <= n; ++i) printf("%lld\n", 1ll * (mx - dep[i]) * (n + 1) + x[i]);
return 0;
}