夢を見れない僕に花咲いた
5 月 23 日 ~ 6 月 5 日
中间考了个月考和会考模拟,所以其实没几个题
LG P9346 无可奈何花落去
给定 \(n\) 个点的树,每次随机删去一条还在的边,当所有点度数 \(\leq 2\) 时停止,求期望轮数对 \(985661441\) 取模后的值。 \(n \leq 5000\)。
相当于是,随机一个删边的序列,然后求使条件成立的最短前缀期望长度。按期望的定义算,枚举 \(i\),然后计算 \(i\) 次操作后恰好停止的方案数。但是恰好比较麻烦,容斥成计算 \(\leq i\) 次操作停止的方案数,减去 \(< i\) 次操作停止的方案数。前者只需要 \(i\) 次操作后每个点度数 \(\leq 2\),这可以通过简单的树形 DP 在 \(\mathcal{O}(n^2)\) 的时间复杂度内求出,具体来说设 \(f_{i,j,k}\) 为 \(i\) 子树内删了 \(j\) 条边,\(i\) 度数为 \(k \in [0,2]\) 的方案数,转移是一个背包。后者就是 \(<i\) 的恰好方案数之和。总时间复杂度 \(\mathcal{O}(n^2)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 5e3 + 5, mod = 985661441;
int ksm(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1LL * a * a % mod) if (b & 1) res = 1LL * res * a % mod;
return res;
}
int n, fc[N], ifc[N];
vector <int> e[N];
int sz[N], f[N][N][3], g[N][3];
void dfs(int u) {
sz[u] = 1, f[u][0][0] = 1;
for (int v : e[u]) {
dfs(v);
for (int i = 0; i < sz[u] + sz[v]; i++) g[i][0] = g[i][1] = g[i][2] = 0;
for (int i = 0; i < sz[u]; i++) {
for (int j = 0; j < sz[v]; j++) {
LL val = (1LL * f[v][j][0] + f[v][j][1] + f[v][j][2]) % mod;
for (int k = 0; k < 3; k++)
g[i + j + 1][k] = (g[i + j + 1][k] + 1LL * f[u][i][k] * val % mod) % mod;
val = (val + mod - f[v][j][2]) % mod;
for (int k = 0; k < 2; k++)
g[i + j][k + 1] = (g[i + j][k + 1] + 1LL * f[u][i][k] * val % mod) % mod;
}
}
sz[u] += sz[v];
for (int i = 0; i < sz[u]; i++)
for (int j = 0; j < 3; j++) f[u][i][j] = g[i][j];
}
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
fc[0] = 1;
for (int i = 1; i <= n; i++) fc[i] = 1LL * fc[i - 1] * i % mod;
ifc[n] = ksm(fc[n], mod - 2);
for (int i = n; i >= 1; i--) ifc[i - 1] = 1LL * ifc[i] * i % mod;
for (int i = 2, x; i <= n; i++)
cin >> x, e[x].push_back(i);
dfs(1);
int ans = 0, sum = 0;
for (int i = 0; i < n; i++) {
LL val = (1LL * f[1][i][0] + f[1][i][1] + f[1][i][2]) % mod;
val = 1LL * val * fc[n - 1 - i] % mod * fc[i] % mod * ifc[n - 1] % mod;
val = (val + mod - sum) % mod;
sum = (sum + val) % mod;
ans = (ans + 1LL * i * val % mod) % mod;
}
cout << ans << "\n";
return 0;
}
CF1821F Timber
在 \([1, n]\) 的区间里放 \(m\) 棵树,每棵树的高度为 \(k\)。每棵树都在整点上,且每个点最多只能放一棵树。求有多少种放置树的方法,满足存在一种砍倒树的方案,使得树倒了之后不会越界,也不会有某个点被超过一棵树占据。你可以自由选择树向左倒占据区间 \([x - k, x]\),或者向右倒占据区间 \([x, x + k]\)。答案对 \(998344353\) 取模。 \(n,m,k \leq 3 \times 10^5\)。
做法一:
考虑如何判定一个状态是否合法:从左往右考虑,贪心地往左倒,如果不能再往右倒,能全部倒完就赢了。据此我们设计 DP:\(f_{i,j}\) 表示钦定按照上面的贪心策略,前 \(i\) 棵树覆盖的最右位置是 \(j\) 的方案数。考虑转移,枚举新的最右位置 \(p\):
- 当 \(p \in [j+k+1,j+2k]\) 时,当树根在 \(p-k\) 时可能往右倒,因此两种方向都有可能,故 \(f_{i+1,p} \gets 2 \times f_{i,j}\)。
- 当 \(p \in [j+2k+1,n]\) 时,一定是往左倒的结果,故 \(f_{i+1,p} \gets f_{i,j}\)。
注意到,转移只和 \(i-1\) 有关并且看起来很规整,我们尝试用 GF 来描述这个转移,容易写出转移函数 \(T(x) = \sum\limits_{i=k+1}^{2k}2x^i + \sum\limits_{i \geq 2k+1}x^i\)。那么答案即为 \(\sum\limits_{i=0}^n [x^i] T(x)^m\),直接用多项式快速幂可以做到 \(\mathcal{O}(n \log n)\)。
当然还可以再给力一点,我们继续推导:\([x^j]T(x)^m = [x^{j-m(k+1)}] \left( \sum\limits_{i=0}^{k-1}2x^i + \sum\limits_{i \geq k}x^i \right)^m\),设括号里的 GF 是 \(F\),我们尝试得到它的封闭形式。容易得到 \(F = xF+2-x^k\),由此可知 \(F = \dfrac{2-x^k}{1-x}\)。故 \([x^j]T(x)^m = [x^{j-m(k+1)}]F(x)^m\)。
设 \(A(x) = (2-x^k)^m\),\(B(x) = (1-x)^{-m}\),那么所求即是 \(A(x)B(x)\) 的前 \(n-m(k+1)\) 项系数。这时候就可能有人会说了,主播主播,这不是还得 FFT 吗?你先别急,\(A(x)\) 的系数我们可以用二项式定理得到 \([x^{ik}]A(x) = (-1)^i 2^{m-i} \binom{m}{i}\),而对于 \(B(x)\),使用广义二项式定理也可以得到 \([x^i]B(x) = \binom{i + m-1}{m-1}\)。当然如果你不会广义二项式定理,也可以把 \(B(x)\) 写成 \(\big( \sum\limits_{i\geq 0}x^i \big)^m\),发现它也对应一个 DP,其组合意义是 \(\sum\limits_{j=1}^m x_j = i\) 的非负整数解数,使用隔板法可以得到同样的结果。
则答案即为:
其中第三个等号运用了组合数的上指标求和公式。带入计算即可,总时间复杂度 \(\mathcal{O}(n)\)。
做法二:
考虑对倒下后的形态进行计数,易知答案是 \(\binom{n-mk}{m}\)。而每棵树有两种倒法,于是答案似乎就是 \(2^m \binom{n-mk}{m}\) 了。但显然这样是会算重的。
考虑容斥,枚举钦定有几棵树能够任意往两边倒。可以推出和做法一一致的式子,时间复杂度 \(\mathcal{O}(n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 5e5 + 5, mod = 998244353;
int ksm(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1LL * a * a % mod) if (b & 1) res = 1LL * res * a % mod;
return res;
}
int n, m, k, fc[N], ifc[N], pw2[N];
int bin(int n, int m) {
if (n < m) return 0;
if (m < 0) return 0;
return 1LL * fc[n] * ifc[m] % mod * ifc[n - m] % mod;
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
if (n < 1LL * m * k) return cout << "0\n", 0;
fc[0] = 1;
for (int i = 1; i <= n; i++) fc[i] = 1LL * fc[i - 1] * i % mod;
ifc[n] = ksm(fc[n], mod - 2);
for (int i = n; i >= 1; i--) ifc[i - 1] = 1LL * ifc[i] * i % mod;
pw2[0] = 1;
for (int i = 1; i <= n; i++) pw2[i] = 1LL * pw2[i - 1] * 2 % mod;
int ans = 0;
for (int i = 0; i <= m; i++) {
int coef = (i & 1) ? mod - 1 : 1;
ans = (ans + 1LL * bin(m, i) * bin(n - m * (k + 1) - i * k + m, m) % mod * pw2[m - i] % mod * coef % mod) % mod;
}
cout << ans << "\n";
return 0;
}
CF1832F Zombies
给定 \(n\) 个左闭右开区间 \(I_i = [l_i, r_i)\)。找出 \(k\) 个左闭右开区间 \(J_j = [a_j, b_j)\) 满足 \(a_j + m = b_j\) 且 \(0\leq a_j < b_j \leq x\),以及一个长度为 \(n\) 的数组 \(p_i\in [1, k]\),求 \(\sum_{i = 1} ^ n |[0, x) \backslash (I_i\cup J_{p_i})|\) 的最大值。\(k\leq n\leq 2 \times 10^3\),\(m\leq x \leq 10 ^ 9\)。
先转化成最大化交集之和。由于 \(J_i\) 的长度固定为 \(m\),那么 \(I_i\) 一定会去和中点相距最近的 \(J_i\) 匹配,于是将 \(I_i\) 按中点排序后,每个 \(J_i\) 会匹配一段区间的 \(I_i\)。容易发现,一定存在一组最优方案使得每个 \(J_i\) 都至少有一个端点和某个 \(I_i\) 的某个端点重合,这点通过调整容易说明。
于是所有可能的 \(J_i\) 只有 \(\mathcal{O}(n)\) 种。考虑 DP,设 \(f_{i,j}\) 用 \(i\) 个 \(J\) 覆盖前 \(j\) 个 \(I\) 的答案,则有转移:\(f_{i,j} = \max\limits_{k=1}^j f_{i-1,k-1} + w(k,j)\),其中 \(w(i,j)\) 表示用一个 \(J\) 匹配 \(I_{i \sim j}\) 的最大贡献。这看起来就很满足四边形不等式,而事实上确实如此,于是如果能够快速计算 \(w(i,j)\),那么直接四边形不等式就能做到 \(\mathcal{O}(n^2)\) 了。
剩下的问题是,怎样快速地求出 \(w(i,j)\)。直接枚举是 \(\mathcal{O}(n^4)\) 的,预处理出每个 \(J\) 和前 \(j\) 个 \(I\) 的交集和,则可以通过差分做到 \(\mathcal{O}(n^3)\)。进一步地,设 \(q_{i,j}\) 为 \(w(i,j)\) 的最优决策点,则总有 \(q_{i,j-1} \leq q_{i,j} \leq q_{i+1,j}\)。使用和四边形不等式相同的优化方法即可做到 \(\mathcal{O}(n^2)\)。
一点细节:若 \(q_{i,i}\) 初始时任取,可能会导致 \(q_{i,i} > q_{i+1,i+1}\)。求 \(q_{i,i}\) 时枚举决策的左边界设为 \(q_{i-1,i-1}\) 即可。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 2e3 + 5;
int n, k, x, m; LL ans;
struct seg {
int l, r;
bool operator < (const seg &z) const {
return l + r < z.l + z.r;
}
} c[N];
int L, d[N << 1], p[N][N];
LL s[N << 1][N], w[N][N], f[N][N];
int calc(int id, int x) {
int l = max(c[id].l, x), r = min(c[id].r, x + m);
return max(0, r - l);
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> k >> x >> m;
for (int i = 1; i <= n; i++) {
cin >> c[i].l >> c[i].r;
ans += x - m - (c[i].r - c[i].l);
d[++L] = c[i].l, d[++L] = c[i].r - m;
}
sort(c + 1, c + n + 1);
sort(d + 1, d + L + 1);
L = unique(d + 1, d + L + 1) - d - 1;
for (int i = 1; i <= L; i++)
for (int j = 1; j <= n; j++)
s[i][j] = s[i][j - 1] + calc(j, d[i]);
for (int len = 1; len <= n; len++) {
for (int i = 1, j = len; j <= n; i++, j++) {
int l = max(p[i - 1][j - 1], p[i][j - 1]), r = p[i + 1][j];
if (len == 1) r = L;
if (l == 0) l = 1;
w[i][j] = -1e18;
for (int k = l; k <= r; k++) {
LL val = s[k][j] - s[k][i - 1];
if (val > w[i][j]) w[i][j] = val, p[i][j] = k;
}
}
}
for (int i = 1; i <= k; i++) {
p[i][n + 1] = L;
for (int j = n; j >= 1; j--) {
int l = p[i - 1][j], r = p[i][j + 1];
if (l == 0) l = 1;
for (int z = l; z <= r; z++) {
if (z > j) break;
LL val = f[i - 1][z - 1] + w[z][j];
if (val > f[i][j]) f[i][j] = val, p[i][j] = z;
}
}
}
cout << ans + f[k][n] << "\n";
return 0;
}
CF1827B Range Sorting
定义一个序列的权值进行若干次操作使序列有序的最小代价,其中每次操作可以选择一个区间 \([x,y]\),以 \(y-x\) 的代价将其中的元素排序。给定长为 \(n\) 的序列 \(a\),求 \(a\) 所有子序列的权值。\(n \leq 3 \times 10^5\)。
先考虑一个序列的贡献怎么求。我们将贡献拆到间隔 \(i-1 \sim i\) 处,间隔 \(i-1 \sim i\) 产生贡献当且仅当 \(a_1 \sim a_{i-1}\) 的最大值大于 \(a_i \sim a_n\) 的最小值,因为此时无论如何都需要交换间隔前后的元素,反之则一定不需要。
枚举间隔 \(i-1 \sim i\) 和区间左端点 \(j < i\)。设 \(v = \max a_{j \sim i-1}\)。
- 若 \(v > a_i\),则 \(i \sim n\) 作为右端点均合法。
- 若 \(v \leq a_i\),则 \(p \sim n\) 作为右端点合法,其中 \(p\) 为 \(i\) 之后第一个小于 \(v\) 的位置,若不存在则为 \(n+1\)。
由于条件和当前的后缀 \(\max\) 相关,我们考虑单调栈,只有单调栈内的元素可能成为后缀 \(\max\)。若栈顶 \(j_1\) 满足 \(a_{j_1} < a_i\),则需要将栈顶弹出。设弹出前栈顶下方的元素是 \(j_2\),那么 \(j_2 + 1\sim j_1\) 每个位置作为左端点时,与 \(i-1\) 之间的最大值都是 \(a_{j_1}\)。那么这些位置的贡献可以一起计算,我们查询一次 \(i\) 之后第一个小于 \(a_{j_1}\) 的位置,把贡献乘上对应左端点的数量即可。
不断弹出直到 \(a_{j_1} > a_i\),此时答案加上 \(j_1(n-i+1)\),因为 \(i-1\sim i\) 在左端点属于 \([1,j_1]\),右端点属于 \([i,n]\) 的区间中产生贡献。
最后的问题是怎么处理形如 "\(i\) 之后第一个小于 \(a_j\) 的位置" 的查询 \((i,j)\)。根据上面的分析,这样的查询至多会有 \(\mathcal{O}(n)\) 个。考虑离线倒序扫描线,把 \((i,j)\) 扔到位置 \(i\) 上处理,把 \(a\) 离散化之后用树状数组维护每个权值对应的最小下标,相当于要单点取 \(\min\) 前缀求 \(\min\),都容易维护。总时间复杂度 \(\mathcal{O}(n \log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef pair <int, int> pi;
typedef long long LL;
constexpr int N = 5e5 + 5;
int n, a[N], b[N], k; LL ans;
struct BIT {
int c[N];
void clr(int n) { fill(c, c + n + 1, 0); }
void init(int n) { fill(c, c + n + 1, n + 1); }
void mdf(int x, int v) {
for (int i = x; i <= n; i += i & -i) c[i] = min(c[i], v);
}
int qry(int x) {
int res = n + 1;
for (int i = x; i; i -= i & -i) res = min(res, c[i]);
return res;
}
} t;
vector <pi> q[N];
void clr() {
ans = k = 0, t.clr(n);
for (int i = 1; i <= n; i++) q[i].clear();
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], b[i] = a[i];
sort(b + 1, b + n + 1);
k = unique(b + 1, b + n + 1) - b - 1;
for (int i = 1; i <= n; i++) a[i] = lower_bound(b + 1, b + k + 1, a[i]) - b;
b[k = 1] = 0, a[0] = n + 1;
for (int i = 1; i <= n; i++) {
while (a[b[k]] < a[i]) {
if (k > 1) q[i].emplace_back(a[b[k]] - 1, b[k] - b[k - 1]);
k--;
}
ans += 1LL * b[k] * (n - i + 1);
b[++k] = i;
}
t.init(n);
for (int i = n; i >= 1; i--) {
t.mdf(a[i], i);
for (auto [v, w] : q[i]) ans += 1LL * (n - t.qry(v) + 1) * w;
}
cout << ans << "\n";
clr();
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
CF1827C Palindrome Partition
称一个字符串是好的,当且仅当它是一个长度为偶数的回文串或由若干长度为偶数的回文串拼接而成。给定一个长度为 \(n\) 的字符串 \(s\),求有多少 \(s\) 的子串是好的。\(n \leq 5 \times 10^5\)。
对于一个偶回文串,若它有一个偶回文串后缀,那么它一定可以划分为若干更短的偶回文串。
因此,对于每个好串,将其划分为极小偶回文串的方式是唯一的:每次找到最短偶回文串后缀,然后删去。据此可以设计 DP:设 \(t_i\) 表示以 \(i\) 结尾的最短偶回文串长度,\(f_i\) 表示以 \(i\) 结尾的好串数量,若 \(t_i\) 不存在则 \(f_i=0\),否则 \(f_i = f_{i - t_i} + 1\),最后答案即为 \(\sum f_i\)。
考虑如何求出 \(t_i\),这只需要求出满足条件的偶回文串的中点 \(d_i\)。用 Manacher 求出以每个间隔为回文中心的最大回文半径,假设 \(i\) 和 \(i+1\) 的间隔最大回文半径为 \(L_i\),那么对于 \(j \in [i+1,i+L_i]\),其一定存在以 \(i\) 和 \(i+1\) 间隔为回文中心的偶回文串,因此 \(d_j \gets \max(d_j, i)\)。从小到大做的话相当于是做区间覆盖,但显然可以倒过来考虑,就变成对区间内的元素赋值,然后把它们删掉。用并查集维护下一个未被删掉的元素即可做到 \(\mathcal{O}(n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 1e6 + 5;
int n, m, p[N], d[N], f[N]; char a[N], b[N]; LL ans;
int pa[N];
int get(int x) { return x == pa[x] ? x : pa[x] = get(pa[x]); }
void cov(int l, int r, int v) {
for (int i = get(l); i <= r; i = get(i + 1)) d[i] = v, pa[i] = get(i + 1);
}
void solve() {
cin >> n;
scanf("%s", a + 1);
b[m = 1] = '|', ans = 0;
for (int i = 1; i <= n; i++) b[++m] = a[i], b[++m] = '|';
int mid = 0, r = 0;
for (int i = 1; i <= m; i++) {
if (i <= r) p[i] = min(p[2 * mid - i], r - i);
else p[i] = 1;
while (i - p[i] >= 1 && i + p[i] <= m && b[i - p[i]] == b[i + p[i]]) p[i]++;
if (i + p[i] > r) mid = i, r = i + p[i];
}
for (int i = 1; i <= n + 1; i++) pa[i] = i, d[i] = 1e9;
for (int i = m; i >= 1; i -= 2) cov(i / 2 + 1, i / 2 + p[i] / 2, i / 2);
for (int i = 1; i <= n; i++) d[i] = 2 * d[i] + 1 - i, f[i] = 0;
for (int i = 1; i <= n; i++) if (d[i] <= i) f[i] = f[d[i] - 1] + 1;
for (int i = 1; i <= n; i++) ans += f[i];
cout << ans << "\n";
}
signed main() {
// ios :: sync_with_stdio(false);
// cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
CF1827D Two Centroids
有一棵树,初始时只有根节点。\(n\) 次操作,每次加叶子之后求至少再添加多少个叶子能使得树有两个重心。\(n \leq 5 \times 10^5\)。
有两个重心,即存在某一条边把树恰好分成了两个大小均为 \(\frac{|V|}{2}\) 的部分。那么对于每一条边,其成为答案的代价是两部分大小之差的绝对值。
但这个东西并不太能够维护,我们继续分析性质。不妨假设当前不存在两个重心,那么这条边的两端一定有一个部分点数 \(< \lfloor \frac{|V|}{2} \rfloor\) 。此时如果 \(> \lfloor \frac{|V|}{2} \rfloor\) 的部分仍存在一颗子树 \(> \lfloor \frac{|V|}{2} \rfloor\),那么整体向那个子树移动一步一定更优。由此可知,最优的边一定至少有一个端点是原树的重心。
于是我们考虑重心,设 \(k\) 是重心的最大子树大小,那么答案就是 \(n - 2k\)。考虑在加叶子的过程中重心是如何变化的:假设当前重心是 \(u\),往子树 \(v\) 中加了一个叶子,若 \(sz_v > \lfloor \frac{|V|}{2} \rfloor\),那么 \(sz_v = \lfloor \frac{|V|}{2} \rfloor + 1\),并且显然 \(u\) 其他子树大小都 \(< \lfloor \frac{|V|}{2} \rfloor\),那么重心会下移到 \(v\),然后不再移动。于是我们发现,对于每次加叶子操作,重心至多会移动一条边,只需要维护当前的重心和 \(k\),本题就做完了。
由于可以离线,考虑提前建出整棵树,用树状数组维护子树大小。假设当前重心 \(u\),若 \(i\) 在 \(u\) 子树内,我们倍增跳到对应的子树 \(v\),用当前的 \(sz_v\) 更新 \(k\)。若 \(k > \lfloor \frac{|V|}{2} \rfloor\),那么将 \(u \gets v\),\(k \gets n - sz_v\)。在 \(u\) 的子树外类似维护即可。总时间复杂度 \(\mathcal{O}(n \log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 5e5 + 5;
int n, par[N]; vector <int> e[N];
int st[N], ed[N], tim, dep[N], f[N][20];
void dfs(int u) {
st[u] = ++tim;
for (auto v : e[u]) {
dep[v] = dep[u] + 1, f[v][0] = u;
for (int j = 1; j <= 19; j++) f[v][j] = f[f[v][j - 1]][j - 1];
dfs(v);
}
ed[u] = tim;
}
int climb(int u, int v) {
int k = dep[v] - dep[u] - 1;
for (int i = 19; i >= 0; i--) if ((k >> i) & 1) v = f[v][i];
return v;
}
struct BIT {
int c[N];
void clr(int n) { fill(c, c + n + 1, 0); }
void mdf(int x, int v) {
for (int i = x; i <= n; i += i & -i) c[i] += v;
}
int qry(int x) {
int res = 0;
for (int i = x; i; i -= i & -i) res += c[i];
return res;
}
int qry(int l, int r) {
return qry(r) - qry(l - 1);
}
} t;
void clr() {
tim = 0, t.clr(n);
for (int i = 1; i <= n; i++) e[i].clear();
}
void solve() {
cin >> n;
for (int i = 2; i <= n; i++) {
cin >> par[i];
e[par[i]].push_back(i);
}
dfs(1);
int root = 1, mx = 0;
t.mdf(1, 1);
for (int i = 2; i <= n; i++) {
t.mdf(st[i], 1);
int ans = 0;
if (st[root] <= st[i] && st[i] <= ed[root]) {
int v = climb(root, i);
int s = t.qry(st[v], ed[v]);
mx = max(mx, s);
if (mx > i / 2) root = v, mx = i - s;
} else {
int s = t.qry(st[root], ed[root]);
mx = max(mx, i - s);
if (mx > i / 2) root = par[root], mx = s;
}
ans = i - 2 * mx;
cout << ans << " ";
}
cout << "\n";
clr();
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
CF1827E Bus Routes
给定一棵 \(n\) 个节点的树。再给定 \(m\) 条路径,每条路径 \((u,v)\) 表示 \(u,v\) 两点间简单路径上的点可以互相到达。判断对于任意两个城市,是否能通过不超过两条路径到达。如果不能,输出两个这样的城市。\(n,m \leq 5 \times 10^5\)。
特判 \(n=2\)。我们发现,叶子的限制严格强于其他点,只需要考虑叶子是否满足条件。
以任意一个点 \(r\) 为根。考虑一个叶子 \(l\),设 \(anc(l)\) 表示 \(l\) 通过一条链能够到达的最浅祖先。显然如果存在两个 \(anc(l_1),anc(l_2)\) 不是祖先后代关系,那么 \(l_1,l_2\) 就无法通过两条链连通,答案是 NO
。
否则,我们只需要保留一个最深的 \(anc(l)\),设为 \(v\)。如果存在一个点 \(u\) 无法一步到达 \(v\),那么答案就是 NO
,否则答案就是 YES
。
下面我们说明这个做法的正确性。假设 \(v\) 对应的叶子是 \(l_1\)。如果存在一个 \(anc(l)\) 更浅的 \(l\) 满足其不能在 \(2\) 步内到达其它叶子,但是 \(l_1\) 可以,我们设 \(l_2\) 是所有这样的叶子中 \(anc(l)\) 最深的一个,且不能到达的叶子是 \(l_3\),由于 \(anc(l_2)\) 的深度比 \(anc(l_3)\) 大,并且它们形成了祖先后代关系,那么 \(l_3\) 一定不在 \(anc(l_2)\) 的子树中。也就是说 \(l_3\) 一定在 \(anc(l_2)\) 的子树外,并且不存在以 \(l_3\) 为端点的链经过 \(anc(l_2)\)。而 \(anc(l_1)\) 在 \(anc(l_2)\) 子树内,因此,若 \(l_2\) 不可达,则 \(l_1\) 也不可达,矛盾。
关于最后一步的实现,一个简单的办法是以 \(v\) 为根再求一遍 \(anc\),若所有点的 \(anc\) 都是 \(v\) 则答案为 YES
。时间复杂度 \(\mathcal{O}(n \log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair <int, int> pi;
constexpr int N = 1e6 + 5;
int n, m, t[N][20], d[N], cu, cv;
vector <int> e[N], v[N], leaf;
void dfs(int u, int ff) {
d[u] = d[ff] + 1, t[u][0] = ff;
for (int i = 1; i <= 19; i++) t[u][i] = t[t[u][i - 1]][i - 1];
for (auto v : e[u]) {
if (v == ff) continue;
dfs(v, u);
}
if (e[u].size() == 1) leaf.push_back(u);
}
int lca(int x, int y) {
if (d[x] < d[y]) swap(x, y);
for (int i = 19; i >= 0; i--) if (d[t[x][i]] >= d[y]) x = t[x][i];
if (x == y) return x;
for (int i = 19; i >= 0; i--) if (t[x][i] != t[y][i]) x = t[x][i], y = t[y][i];
return t[x][0];
}
void clr() {
cu = cv = 0, leaf.clear();
for (int i = 1; i <= n; i++) d[i] = 0, e[i].clear(), v[i].clear();
for (int i = 0; i <= n; i++) for (int j = 0; j <= 19; j++) t[i][j] = 0;
}
void solve() {
cin >> n >> m;
clr();
for (int i = 1, x, y; i < n; i++) {
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
bool ok = 0;
for (int i = 1, x, y; i <= m; i++) {
cin >> x >> y;
v[x].push_back(y);
v[y].push_back(x);
if (x == 1 && y == 2) ok = 1;
if (x == 2 && y == 1) ok = 1;
}
if (n == 2) {
if (m > 0) {
if (ok) return cout << "YES\n", void();
}
return cout << "NO\n" << "1 2\n", void();
}
vector <pi> anc;
int root = 1;
dfs(root, 0);
for (auto x : leaf) {
int mi = 1e9, it = 0;
for (auto y : v[x]) {
int r = lca(x, y);
if (d[r] < mi) it = r, mi = d[r];
}
if (it == 0) {
cu = x;
for (auto y : leaf) if (y != x) cv = y;
return cout << "NO\n" << cu << " " << cv << "\n", void();
}
anc.emplace_back(it, x);
}
sort(anc.begin(), anc.end(), [&](pi x, pi y) {
return d[x.first] > d[y.first];
});
for (int i = 1; i < anc.size(); i++) {
int x = anc[i].first;
int y = anc[i - 1].first;
if (lca(x, y) != x) {
cu = anc[i].second, cv = anc[i - 1].second;
return cout <<"NO\n" << cu << " " << cv << "\n", void();
}
}
root = anc[0].first;
d[root] = 0;
for (int i = 0; i <= n; i++) for (int j = 0; j <= 19; j++) t[i][j] = 0;
leaf.clear();
dfs(root, 0);
for (auto x : leaf) {
int mi = 1e9, it = 0;
for (auto y : v[x]) {
int r = lca(x, y);
if (d[r] < mi) it = r, mi = d[r];
}
if (it != root) {
cu = x, cv = anc[0].second;
return cout << "NO\n" << cu << " " << cv << "\n", void();
}
}
cout << "YES\n";
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
int t; cin >> t;
while (t--) solve();
return 0;
}
QOJ4812 Counting Sequence
定义一个序列 \(a_{1 \sim m}\) 是好的当且仅当其满足:\(a_i > 0\),\(|a_i - a_{i-1}| = 1\),且 \(\sum\limits_{i=1}^m a_i = n\)。对于一个好序列,定义其权值 \(f(a) = \sum\limits_{i=1}^{m-1} [a_i > a_{i+1}]\)。给定常数 \(c\),求所有好序列的 \(c^{f(a)}\) 之和。\(n \leq 3 \times 10^5\)。
首先有一个显然的 DP:设 \(f_{i,j}\) 为当前总和为 \(i\),上一个元素是 \(j\) 的所有序列的权值和,转移比较平凡,但是这是 \(\mathcal{O}(n^2)\) 的。
注意到总和是一个常数,毛估估一下长度和权值不能同时较大,于是考虑对 \(a_1\) 大小分治。令 \(B = \sqrt{2n} + \mathcal{O}(1)\)。则当 \(a_1 > B\) 时,有 \(m < B\),此时无需考虑 \(a_i > 0\) 这个限制。因此可以换个方向做 DP:设 \(g_{i,j}\) 表示不考虑 \(a_i > 0\) 这个限制,满足 \(a_1 = 0\),总和为 \(j\) 的所有长度为 \(i\) 的序列的权值和,转移考虑每次把所有数加 \(1\) 或减 \(1\),然后在最前面放一个 \(0\),不难列出转移方程。计算答案时枚举 \(a_1\) 和 \(m\) 即可。
当 \(a_1 < B\) 时,序列的最大元素不会超过 \(\mathcal{O}(\sqrt{n})\),沿用 \(f\) 的计算方式即可。总时间复杂度 \(\mathcal{O}(n \sqrt n)\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
constexpr int N = 3e5 + 10, B = 800, mod = 998244353;
void add(int &a, int b) { a = (a + b >= mod) ? (a + b - mod) : (a + b); }
int n, c, ans;
namespace small {
constexpr int M = 1.3e3 + 10, L = 1.3e3;
int f[M][M];
void Main() {
for (int i = 1, o = 1; i <= n; i++, o = (o + 1) % M) {
fill(f[o] + 1, f[o] + L + 1, 0);
if (i <= B) f[o][i] = 1;
for (int j = 1; j <= L; j++) {
add(f[o][j], f[(o + M - j) % M][j - 1]);
add(f[o][j], 1LL * f[(o + M - j) % M][j + 1] * c % mod);
}
}
for (int i = 1; i <= L; i++) add(ans, f[n % M][i]);
}
}
namespace large {
int c2[N], f[2][B * B];
void Main() {
for (int i = 1; i <= B; i++) c2[i] = c2[i - 1] + i - 1;
f[1][c2[B]] = 1;
for (int i = 1, o = 1; i < B; i++, o ^= 1) {
fill(f[o ^ 1], f[o ^ 1] + 2 * c2[B], 0);
for (int x = B + 1; i * x - c2[i] <= n; x++) {
int s = n - i * x;
if (-c2[i] <= s && c2[i] >= s) add(ans, f[o][s + c2[B]]);
}
for (int s = -c2[i]; s <= c2[i]; s++) if (f[o][s + c2[B]]) {
add(f[o ^ 1][s + i + c2[B]], f[o][s + c2[B]]);
add(f[o ^ 1][s - i + c2[B]], 1LL * f[o][s + c2[B]] * c % mod);
}
}
}
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> c;
small :: Main();
large :: Main();
cout << ans << "\n";
return 0;
}