ARC193C Grid Coloring 3
考虑题目中给出的十字覆盖正着计数很困难,所以正难则反,倒过来做。
考虑反过来的第一次操作相当于把一个颜色相同的十字上的颜色都换成 0(我们把 0 定义为可以表示任何数的颜色)。进一步,我们考虑恰好有 r 行 c 列为“主色”,我们一定可以把它们全部变成 0。
接着我们就可以只改变一行或一列的颜色了,比如我们要染一行,就找一个“主色”列,对于这一行和这一列操作。由于操作逆序,所以“主色”列总会被覆盖为主色。这样我们可以直接把主色的 r 行 c 列给删掉,把问题转化成在一个 n×m 的网格中,每次删一列或是一行。当然为了符合原问题,我们有一些限制,即 n×m 网格中不能存在一行或一列均为“主色”。
我们可以考虑 DP,令 fn,m,0/1 表示删减 n×m 的网格,以及是否有主色的限制。为了方便,我们将这一维 0/1 称作 t。
接着思考怎么转移。首先如果分行、列两种情况讨论的话十分麻烦,我们不妨只考虑行,如果要考虑列时我们把矩阵旋转一下让列变成行即可。那么假设我们选择了其中 r 行,由于我们只能删除颜色相同的一行,所以这 r 行内每个行内的格子颜色都要相等。再考虑“主色”的限制,这 r 行的情况就是 (C−t)r,其中 C 为题目中的颜色限制。那么剩下的 n−r 行中我们只能选择列了,并且由于前 r 行都不是主色,所以我们选择列的时候就可以不考虑主色,即 fm,n−r,0。
但此时又有一个限制是这 n−r 行中不能每一行的颜色不能相同,所以我们需要再加一个维度 0/1 表示是否限定当前每一列颜色相同。则我们要分为 fn,m,t,0 和 fn,m,t,1 两种情况转移。对于 fn,m,t,0 根据上述解释可以从 (C−t)r×fm,n−r,0,1 转移过来;对于 fn,m,t,1 我们还要分两种情况讨论。
一种是枚举的 r 行的颜色全部相同,这种情况的方案数就是 (C−t)×fm,n−r,1,1。原因是我们定义的 t=1 这维限制每一行、每一列不全为钦定色,另一维则限制每一列颜色不全相同,t=1 其实就起到了每一行不全为钦定色的效果。
另一种是枚举的 r 行颜色不全相同,这种情况的转移就是 [(C−t)r−(C−t)]×fm,n−r,0,1。
然后要注意我们所有的枚举 r 行操作都要乘上一个 Crn,所以我们在 DP 前预处理一下组合数和 C 的次方即可。
const int N = 410, Mod = 998244353;
LL n, m, c;
LL f[N][N][2][2], pw[2][N], C[N][N];
void init() {
rep(i, 0, N - 1) {
C[i][0] = 1;
rep(j, 1, i) C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % Mod;
}
pw[0][0] = pw[1][0] = 1;
rep(i, 1, N - 1) {
pw[0][i] = pw[0][i - 1] * c % Mod;
pw[1][i] = pw[1][i - 1] * (c - 1) % Mod;
}
memset(f, -1, sizeof f);
}
LL dp(int n, int m, int t, int p) {
LL &u = f[n][m][t][p];
if (u != -1) return u;
u = pw[t][n];
if (p) u = (u + Mod - (c - t)) % Mod;
rep(r, 1, n - 1) {
LL tmp = pw[t][r], tmp2 = c - t, res = 0;
if (!p) res = (res + tmp * dp(m, n - r, 0, 1) % Mod) % Mod;
else {
res = (res + (tmp - tmp2) * dp(m, n - r, 0, 1) % Mod) % Mod;
res = (res + tmp2 * dp(m, n - r, 1, 1) % Mod) % Mod;
}
u = (u + res * C[n][r] % Mod) % Mod;
}
return u;
}
int main() {
n = read(), m = read(), c = read();
c %= Mod;
init();
LL ans = 1;
rep(i, 1, n - 1) rep(j, 1, m - 1) {
LL tmp = C[n][i] * C[m][j] % Mod;
LL tmp2 = (dp(n - i, m - j, 1, 0) + dp(m - j, n - i, 1, 1)) % Mod;
ans = (ans + tmp * tmp2 % Mod) % Mod;
}
printf("%lld\n", ans * c % Mod);
return 0;
}
P12012 [Ynoi April Fool’s Round 2025] 牢爱
注意到有效的查询长度是很短的,原因是设长度为 len,当 2len>v×len 时根据鸽巢原理就一定是有解的,v 上限为 1000,所以当 len>13 时无脑 Yuno
。
对于 len≤13 的部分我们可以做一个 DP,设 fi,j 表示用前 i 个数能否凑出 j,有转移 fi,j=fi−1,j|fi−1,j−ai−1。
判断能否取出两个无交子集我们只需判断是否存在 fi−1,j 和 fi−1,j−ai−1 同时为 True
即可。因为在这种情况下我们给右边的集合取一个 ai,然后再把两个集合的重复部分删掉就成功了。
然后对于这个修改可以使用拓展欧拉定理:a^b \equiv a^{b \bmod \varphi(n) + \varphi(n)} \pmod n,这可以在 数学 - 比翼の鼠 中找到。
煮波煮波拓展欧拉定理还是太吃操作了,有没有更简单的做法。有的兄弟,有的。
不妨暴力一些直接预处理出 g_{v, i} 表示 v^3 再开 i 次立方的结果,然后对于每次区间立方的操作我们用一个树状数组维护一下即可。
其实拓展欧拉定理的方式也要用树状数组维护。
const int N = 100010;
int n, m, v;
int a[N], val[2010][25];
bitset<15010> state;
struct BIT {
int tr[N];
BIT() { memset(tr, 0, sizeof tr); }
void add(int x, int k) {
for (; x <= n; x += lowbit(x)) tr[x] += k;
}
int query(int x) {
int res = 0;
for (; x; x -= lowbit(x)) res += tr[x];
return res;
}
} btree;
int calc(int x, int p) {
for (int i = 24; i >= 0; i --)
if (p >> i & 1) x = val[x][i];
return x + 1;
}
signed main() {
n = read(), m = read(), v = read();
for (int i = 1; i <= n; i ++) a[i] = read();
for (int i = 0; i <= v; i ++) val[i][0] = i * i % v * i % v;
for (int j = 1; j <= 24; j ++)
for (int i = 0; i <= v; i ++) val[i][j] = val[val[i][j - 1]][j - 1];
while (m --) {
int op, l, r; op = read(), l = read(), r = read();
if (op == 1) {
if (r - l + 1 > 13) { puts("Yuno"); continue; }
state.reset(); state.set(0, 1);
bool f = false;
for (int i = l; i <= r; i ++) {
int tmp = calc(a[i], btree.query(i));
if ((state & (state << tmp)).any()) { f = true; break; }
state = state | (state << tmp);
}
if (f) puts("Yuno"); else puts("Yuki");
} else {
btree.add(l, 1); btree.add(r + 1, -1);
}
}
return 0;
}
P11971 「ALFR Round 7」T4 xor xor
非常好问题,使我旋转。
通过思考会发现当区间 [l, r] 内 0 和 1 的数量都 \geq k 时答案就是 2^k - 1,否则 0 与 1 一定有一个大于 k,另一个小于 k。
下文以 0 多为例。
多尝试几组数据会发现一定存在最优的构造使得一个数全为 0,而另一个数的理想状态是要把所有的 1 放到高位,但这样可能就凑不足 k 位了。
虽然遭遇挫折但我们注意到这个数的形式一定是前面有一些数然后后面是一段后缀的 0。我们不妨二分出这段后缀 0 的开始,然后就做完了。
const int N = 1000010, Mod = 1e9 + 7;
int n, q, s[N][2], pw[N], h[N][2];
char str[N];
int calc(int l, int r, int c) {
if (l > r) return 0;
return s[r][c] - s[l - 1][c];
}
int calc2(int l, int r, int c) {
return (h[r][c] - h[l - 1][c] * (pw[r - l + 1] + 1) % Mod + Mod) % Mod;
}
signed main() {
n = read(), q = read();
scanf("%s", str + 1);
for (int i = 1; i <= n; i ++) {
int c = str[i] - '0';
s[i][0] = s[i - 1][0] + 1 - c, s[i][1] = s[i - 1][1] + c;
pw[i] = (pw[i - 1] * 2 + 1) % Mod;
h[i][0] = (h[i - 1][0] * 2 + 1 - c) % Mod, h[i][1] = (h[i - 1][1] * 2 + c) % Mod;
}
int Case = 0;
while (q --) {
int l = read(), r = read(), k = read(), c = 0;
if (calc(l, r, 0) >= k) {
if (calc(l, r, 1) >= k) { printf("%d\n", pw[k]); continue; }
else c = 1;
}
int L = l, R = r;
while (L < R) {
int mid = L + R + 1 >> 1;
if (calc(l, mid - 1, c) + r - mid + 1 >= k) L = mid;
else R = mid - 1;
}
printf("%lld\n", (pw[calc(l, L - 1, c)] * (pw[r - L + 1] + 1) % Mod + calc2(L, r, c)) % Mod);
}
return 0;
}
P11955 「ZHQOI R1」覆盖
超级喵喵题,喵喵喵。
先考虑 AB 性质,考虑设 f(k) 表示 n = 2^k 时的答案,这里引用一张学长画的图。
对于当前的第 k 层我们可以将一些 k - 1 层的延续下来,使得除了收尾其余部分都是两两相连。容易得到此时新加的操作数为 2^{k - 2} + 1,所以有 f(k) = f(k - 1) + 2^{k - 2} + 1。
现在考虑如果询问区间怎么做,或者说怎么把 AB 性质进行推广。
我们不妨模拟一下 n 从 [2^{k - 1}, 2^{k} - 1] (记作 [L, R])的线段树形态,我们会发现只有对于前 2^{k - 2} 个数我们需要增加答案,所以我们可以得到 [L, R] 的答案为 f(k - 1) \times (R - L + 1) + \sum_{i = 1}^{2^{k - 2}}(R - L + 1 - i)。后面这段是等差数列,可以直接算。
当然也有可能我们凑不满 [L, R],遇到了 [L, x](x < R) 的形式,此时答案为 f(k - 1) \times (x - L + 1) + \sum_{i = 1}^{2^{k - 2}}max(x - L + 1 - i, 0)。
所以对于所有的询问 [l, r] 我们按照这个过程算一遍即可。单次复杂度为 \operatorname{log}(r),总复杂度为 q\operatorname{log}(r)。
注意中间计算过程会爆 long long
。
const int N = 110, Mod = 353442899;
int q, f[N];
void prework() {
f[1] = 2;
for (int i = 2; i <= 62; i ++) f[i] = (f[i - 1] + (1ll << i - 2) + 1) % Mod;
}
int calc(int x) {
int ans = x;
for (int i = 2; i <= 62; i ++) {
int l = 1ll << i - 1, r = (1ll << i) - 1;
if (l > x) break;
if (r <= x) {
if (i == 2) { ans += 5; continue; }
int tmp = (__int128)(r - l + r - l + 1 - (1ll << i - 2)) * (1ll << (i - 2)) / 2 % Mod;
ans = (ans + (__int128)f[i - 1] * (r - l + 1) % Mod) % Mod;
ans = (ans + tmp) % Mod;
} else {
if (i == 2) { ans += 2; continue; }
int d = min(x - l, (LL)(1ll << i - 2));
int tmp = (__int128)(x - l + x - l + 1 - d) * d / 2 % Mod;
ans = (ans + (__int128)f[i - 1] * (x - l + 1) % Mod) % Mod;
ans = (ans + tmp) % Mod;
}
}
return ans;
}
void solve() {
int l = read(), r = read();
printf("%lld\n", (calc(r) - calc(l - 1) + Mod) % Mod);
}
signed main() {
prework();
q = read();
while (q --) solve();
return 0;
}
P10794 『SpOI - R1』架子鼓可以站 C
首先操作完成后的直径一定要经过根,否则可以进行操作增加答案。
因此我们考虑最后的直径一定是占用根的两个子树,然后我们会发现进行两次以上的操作并无意义。并且根据直觉,进行两次操作一定不劣于一次和零次,所以我们只需要考虑进行两次操作的情况即可。
我们可以将操作分为两种情况,一种是两次操作都在同一个子树中,另一种是在两个不同的子树中。后者只需要分别求两棵子树内的直径,而前者则较为复杂。
先考虑我们要干什么。根据上文推导我们的核心目标是找到两条边集不相等的最长链,而由于我们两次操作都在同一子树中,所以我们相当于要在这个子树中断开一条边,然后将整棵树分为两部分,求最大的直径和。
这很可以树形 DP,首先求出 f_u 表示 u 子树内的直径,g_u 为 u 为根的最长链。
然后你要考虑如何求 u 上方的直径,显然要换根。我们设 k_u 为不包含 u 为根子树的直径,考虑如何从一个 k_u 转移到 k_v,分三种情况。
- k_v = k_u
- 考虑 v 的兄弟子树的内部直径,有 k_v = \max{f_{brother_v}}
- 考虑从兄弟方向和父亲方向选两条最长链,其中父亲方向只能选出一条。我们不妨从父亲方向和兄弟方向处理出前三长的链,然后找出前两条能用的就行了。
实现起来稍微有点细节。
const int N = 200010;
int T, n, res;
vector<int> G[N];
int g[N], f[N], h[N][3], p[N][3], k[N];
vector<int> pre[N], suf[N];
void init() {
memset(h, 0, sizeof h); memset(p, 0, sizeof p);
for (int i = 1; i <= n; i ++) G[i].clear(), pre[i].clear(), suf[i].clear();
}
void upd(int u, int v, int w) {
if (w >= h[u][0]) {
h[u][2] = h[u][1], p[u][2] = p[u][1];
h[u][1] = h[u][0], p[u][1] = p[u][0];
h[u][0] = w, p[u][0] = v;
} else if (w >= h[u][1]) {
h[u][2] = h[u][1], p[u][2] = p[u][1];
h[u][1] = w, p[u][1] = v;
} else if (w > h[u][2]) {
h[u][2] = w, p[u][2] = v;
}
}
void dfs(int u) {
f[u] = g[u] = 0, k[u] = -INF;
int m = G[u].size();
pre[u].resize(m), suf[u].resize(m);
for (int i = 0; i < m; i ++) {
int v = G[u][i];
dfs(v);
f[u] = max(f[u], g[u] + g[v] + 1); f[u] = max(f[u], f[v]);
g[u] = max(g[u], g[v] + 1);
upd(u, v, g[v] + 1);
pre[u][i] = f[v], suf[u][i] = f[v];
}
for (int i = 1; i < m; i ++) pre[u][i] = max(pre[u][i], pre[u][i - 1]);
for (int i = m - 2; i >= 0; i --) suf[u][i] = max(suf[u][i], suf[u][i + 1]);
}
int calc(int u, int v, int cnt) {
int s = 0, res = 0;
for (int i = 0; i < 3; i ++) {
if (p[u][i] == v) continue;
s ++, res += h[u][i];
if (s >= cnt) break;
}
return res;
}
void dp(int u, int d) {
upd(u, 0, d);
int m = G[u].size();
for (int i = 0; i < m; i ++) {
int v = G[u][i];
k[v] = k[u];
if (i) k[v] = max(k[v], pre[u][i - 1]);
if (i + 1 < m) k[v] = max(k[v], suf[u][i + 1]);
k[v] = max(k[v], calc(u, v, 2));
dp(v, calc(u, v, 1) + 1);
}
res = max(res, f[u] + k[u] + 2);
}
void solve() {
init(); n = read();
for (int i = 1; i < n; i ++) G[read()].push_back(i + 1);
int maxv = -INF; res = 0;
for (auto u : G[1]) {
dfs(u);
res = max(res, maxv + f[u] + 2);
maxv = max(maxv, f[u]);
k[u] = -INF;
dp(u, 0);
}
res = max(res, maxv + 1);
printf("%d\n", res);
}
int main() {
T = read();
while (T --) solve();
return 0;
}
AGC025D - Choosing Points
先只对一个 D 进行考虑,会发现此时如果把每个点和所有不能到达的点连边,则相当于选出一个独立集。
设 D = 4^p \times q\ (q \bmod 4 \ne 0),则对于点 (0, 0) 它所连边的点可以表示为 (2^p \times a, 2^p \times b),且满足 a^2 + b^2 = q。此时对于 q 进行讨论。
- 若 p \bmod 4 = 1,则此时由于 a, b \bmod 2 = 0 \ or\ 1,所以 a 和 b 一定不同奇偶。稍加思考会发现一个坐标同奇偶的 (x, y) 一定会连到不同奇偶的点,两个集合独立,所以是一个二分图。
- 若 p \bmod 4 = 2,则此时 a 和 b 一定都是奇数。这时同奇偶的 (x, y) 一定只会向同奇偶的点连边,同样是二分图。
- 若 p \bmod 4 = 3,不存在这种情况。
所以我们可以直接进行二分图染色。对于两个 D 的情况就分别对两个图染色,根据这样每个点的染色最多只有四种情况,根据抽屉原理一定能找到解。
const int N = 1010;
int n, d1, d2;
int c[N][N][2], cnt[2][2];
void work(int op, int d) {
int k = 0;
while (d % 4 == 0) d /= 4, k ++;
if (d % 4 == 3) return;
k = 1 << k;
if (d % 4 == 1) {
rep(i, 1, n) rep(j, 1, n) c[i][j][op] = ((i - 1) / k + (j - 1) / k) & 1;
} else {
rep(i, 1, n) rep(j, 1, n) c[i][j][op] = ((i - 1) / k) & 1;
}
}
int main() {
cin >> n >> d1 >> d2; n <<= 1;
work(0, d1); work(1, d2);
rep(i, 1, n) rep(j, 1, n) cnt[c[i][j][0]][c[i][j][1]] ++;
int x = -1, y = -1;
rep(i, 0, 1) rep(j, 0, 1) if (cnt[i][j] >= n * n / 4) x = i, y = j;
int cnt = 0;
rep(i, 1, n) rep(j, 1, n)
if (c[i][j][0] == x && c[i][j][1] == y) {
printf("%d %d\n", i - 1, j - 1), cnt ++;
if (cnt >= n * n / 4) return 0;
}
if (cnt != n * n / 4) puts("D & A");
return 0;
}
CF2094H
1900 分根号分治题,可惜我没做出来(其实主要还是没想到第二种预处理)。
对于一个询问显然直接循环做没有前途,原因是会遍历很多 useless
状态无法改变 k。不妨考虑如何快速从一个 useful
状态跳到另一个 useful
状态。
首先会发现 k \leq 100000,这代表所有询问实际上都比较小,所以它们的约数也都比较小。比较自然的有一种类似序列自动机的预处理,设 ne_{i, j} 为 i 及它后面 j 第一次出现的位置。转移是轻松的。
这样做是 O(nV) 的。我们考虑这个做法的瓶颈在于值域 V 太大了,所以我们不妨根号分治。只处理值域 \leq B 的情况。而对于值域 > B 的情况我们可以类似筛法地把求出 pos_x 表示 x 在 a 中的所有因数。
然后对于询问,按照最开始提到的思路做即可。需要注意的是我们每次需要在 pos 中二分,所以还要加个 \log。整个时间复杂度是 O(nB + \frac{nV}{B} + q\log V(\log n + B))。B 随便取个 250 就跑的很快了。
const int N = 100010, B = 250;
int n, q, a[N];
int ne[N][B];
vector<int> divs[N], pos[N];
void solve() {
n = read(), q = read();
for (int i = 1; i <= n; i ++) a[i] = read();
for (int i = 1; i <= B; i ++) ne[n + 1][i] = n + 1;
for (int i = 1; i <= n; i ++) {
if (a[i] > B)
for (int j = a[i]; j <= N - 10; j += a[i])
pos[j].push_back(i);
}
for (int i = n; i >= 1; i --) {
for (int j = 1; j <= B; j ++) ne[i][j] = ne[i + 1][j];
if (a[i] <= B) ne[i][a[i]] = i;
}
while (q --) {
int k = read(), l = read(), r = read();
LL ans = 0;
for (int p = l; ; ) {
int R = r + 1;
for (auto x : divs[k])
if (x <= B) R = min(R, ne[p][x]);
auto it = lower_bound(pos[k].begin(), pos[k].end(), p);
if (it != pos[k].end()) R = min(R, *it);
ans += 1ll * (R - p) * k;
p = R;
if (p > r) break;
while (k % a[p] == 0) k /= a[p];
}
printf("%lld\n", ans);
}
for (int i = 1; i <= n; i ++) {
if (a[i] > B)
for (int j = a[i]; j <= N - 10; j += a[i])
pos[j].clear();
}
}
int main() {
for (int i = 2; i <= N - 10; i ++)
for (int j = i; j <= N - 10; j += i)
divs[j].push_back(i);
int T = read();
while (T --) solve();
return 0;
}
CF2093G
简单 Trie 树练习题,Rating 1900,自己做出。
看到异或和最大值显然往 Trie 树方面思考。考虑对于每一个 i 在 trie 上查询 1 \sim i - 1 能满足 a_{j} \oplus a_i \geq k 的最靠近 i 的 j。插入是简单的,查询时根据 k 的每一位分讨,设当前 a_i 这一位的值为 u,k 为 c。则若 c = 0 说明我们往 u \oplus 1 方向走一定满足条件,那么我们应该取 u \oplus 1 方向的最大 j;如果向 u 方向走则继续递归处理。对于 c = 1 则限定我们必须往 u \oplus 1 方向走。
于是我们在插入的时候多维护一个 maxv_p 表示 p 为根子树内的最大 j。
const int N = 200010, M = 32;
int n, k, a[N];
int tr[N * M][2], idx = 1, ed[N * M], maxv[N * M], pre[N * M];
void insert(int num, int id) {
int p = 1; pre[1] = -1;
for (int i = 30; i >= 0; i --) {
int c = num >> i & 1;
if (!tr[p][c]) tr[p][c] = ++ idx;
pre[tr[p][c]] = p, p = tr[p][c];
}
maxv[p] = id;
while (p != -1) {
int q = pre[p];
maxv[q] = max(maxv[q], maxv[p]);
p = q;
}
}
int query(int num, int k) {
int p = 1, ans = 0;
for (int i = 30; i >= 0; i --) {
int c = k >> i & 1, u = num >> i & 1;
if ((!c & 1)) {
ans = max(ans, maxv[tr[p][u ^ 1]]);
p = tr[p][u];
} else {
p = tr[p][u ^ 1];
}
}
ans = max(ans, maxv[p]);
return ans;
}
void init(int p) {
maxv[p] = 0;
if (tr[p][0]) init(tr[p][0]), tr[p][0] = 0;
if (tr[p][1]) init(tr[p][1]), tr[p][1] = 0;
}
void solve() {
init(1); idx = 1;
n = read(), k = read();
rep(i, 1, n) a[i] = read();
int ans = INF;
rep(i, 1, n) {
insert(a[i], i);
int x = query(a[i], k);
if (!x) continue;
ans = min(ans, i - x + 1);
}
printf("%d\n", ans == INF ? -1 : ans);
}
int main() {
int T = read();
while (T --) solve();
return 0;
}
CF2092D
贪心,Rating 1800。
做这个题的时候头有点晕,想了半天没发现 ABC
是一个万能的拓展,所以我们只要构造 ABC
即可,构造不出来就无解。
const int N = 310;
int T, n;
char s[N];
void solve() {
n = read(); scanf("%s", s + 1);
map<char, int> cnt;
bool f = false;
for (int i = 1; i < n; i ++) {
if (s[i] != s[i + 1]) f = true;
cnt[s[i]] ++;
} cnt[s[n]] ++;
if (!f) { puts("-1"); return; }
if (cnt['I'] == cnt['L'] && cnt['L'] == cnt['T']) { puts("0"); return; }
int sum = 'I' ^ 'L' ^ 'T'; vector<int> ans;
for (int i = 2; i <= n; i ++) {
if (s[i - 1] == s[i]) continue;
int x = s[i - 1], z = s[i], y = sum ^ (x ^ z), p = i - 1;
ans.push_back(p); cnt[y] ++;
while (cnt[x] != cnt[y] || cnt[y] != cnt[z] || cnt[x] != cnt[z]) {
if (cnt[x] > cnt[z]) ans.push_back(p), cnt[z] ++, swap(y, z);
else ans.push_back(++ p), cnt[x] ++, swap(x, y);
}
break;
}
printf("%d\n", ans.size());
for (auto x : ans) printf("%d\n", x);
}
int main() {
T = read();
while (T --) solve();
return 0;
}
P8493 [IOI 2022] 数字电路
一个很喵喵的套路。我们考虑为 1 的方案数等于为 1 的概率乘上总方案数。
于是做 DP,令 f_u 表示 u 为 1 的概率,那么考虑当 u 的儿子中有 i 个 1 时,阈值就只能设为 [1, i]。所以 f_u = \sum_{i = 1}^{|\operatorname{son}(u)|}\frac{i}{|\operatorname{son}(u)|}g_{u, i},其中 g_{u, i} 表示 u 的儿子中有 i 个 1 的概率。
你会发现直接算 g_{u, i} 显得不是那么可行,但你会发现 \sum_{i = 1}^{|\operatorname{son}(u)|}i \times g_{u, i} 其实就是 u 中儿子中 1 数量的期望 E(u)。
而 E(u) = \sum_{v \in \operatorname{son}(u)}f_v,所以就的得到了 f_u 到 f_v 的递推式:f_u = \frac{1}{|\operatorname{son}(u)|}\sum_{v \in \operatorname{son}(u)} f_v。
进一步分析,贡献的出发点一定是非阈值点,在题目给出的结构中即为叶子。所以对于 f_0 来说,总的概率就是 \sum_u\prod_{v \in \operatorname{path}(u, 0)}\frac{1}{|\operatorname{son}(v)|}。
当然,这是概率。我们还要再乘总的方案数 \prod_{u}|\operatorname{son}(u)|,会发现这么乘完后答案就是 \sum_{u}\prod_{v \notin \operatorname{path}(u, 0)}。
于是你对于每个叶子预处理一下贡献,存到线段树上。接着维护一下区间翻转即可,每次将还为 1 的叶子的贡献输出即可。
const int N = 200010, Mod = 1000002022;
vector<int> g[N];
int n, m, s[N], f[N], d[N], a[N], p[N];
vector<int> pre, suf;
IL int add(int a, int b) { return (a + b) % Mod; }
IL int mul(int a, int b) { return 1ll * a * b % Mod; }
void add_edge(int a, int b) { g[a].push_back(b); }
void dfs1(int u) {
if (!s[u]) { d[u] = 1; return; }
d[u] = s[u];
for (auto v : g[u]) {
dfs1(v);
d[u] = mul(d[u], d[v]);
}
}
void dfs2(int u, int res) {
f[u] = res;
if (!s[u]) return;
int sz = s[u];
vector<int> pre(sz), suf(sz);
pre[0] = d[g[u][0]], suf[sz - 1] = d[g[u][sz - 1]];
for (int i = 1; i < sz; i ++) {
int v = g[u][i];
pre[i] = mul(pre[i - 1], d[v]);
}
for (int i = sz - 2; i >= 0; i --) {
int v = g[u][i];
suf[i] = mul(suf[i + 1], d[v]);
}
for (int i = 0; i < sz; i ++) {
int t = res, v = g[u][i];
if (i) t = mul(t, pre[i - 1]);
if (i < sz - 1) t = mul(t, suf[i + 1]);
dfs2(v, t);
}
}
struct Node { int l, r, s[2], rev; };
struct Segment_Tree {
Node tr[N << 2];
void pushup(int u) {
tr[u].s[0] = add(tr[u << 1].s[0], tr[u << 1 | 1].s[0]);
tr[u].s[1] = add(tr[u << 1].s[1], tr[u << 1 | 1].s[1]);
}
void reverse(int u) {
tr[u].rev ^= 1;
swap(tr[u].s[0], tr[u].s[1]);
}
void pushdown(int u) {
if (tr[u].rev) {
reverse(u << 1); reverse(u << 1 | 1);
tr[u].rev ^= 1;
}
}
void build(int u, int l, int r) {
tr[u] = {l, r, {0, 0}, 0};
if (l == r) { tr[u].s[a[l - 1]] = f[l + n - 1]; return; }
int mid = l + r >> 1;
build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r);
pushup(u);
}
void modify(int u, int l, int r) {
if (l <= tr[u].l && tr[u].r <= r) {
reverse(u);
return;
}
pushdown(u);
int mid = tr[u].l + tr[u].r >> 1;
if (l <= mid) modify(u << 1, l, r);
if (r > mid) modify(u << 1 | 1, l, r);
pushup(u);
}
int query() { return tr[1].s[1]; }
} sgt;
void init(int N, int M, vector<int> P, vector<int> A) {
n = N, m = M;
for (int i = 1; i < n + m; i ++) add_edge(P[i], i);
for (int i = 0; i < n + m; i ++) s[i] = g[i].size();
for (int i = 0; i < m; i ++) a[i] = A[i];
dfs1(0); dfs2(0, 1);
sgt.build(1, 1, m);
}
int count_ways(int L, int R) {
sgt.modify(1, L - n + 1, R - n + 1);
return sgt.query();
}
P5749 [IOI 2019] 排列鞋子
贪心性质比较一眼,但维护部分最开始写出了神秘做法。
考虑从前往后遍历数组,如果遇到了左脚就把后面的右脚放到左脚后面,否则把左脚放到右脚前面。这样一定是最优的。
暴力交换是 O(n^2) 的,但注意到操作相当于平移了一部分鞋子的下标,直接在树状数组上差分区间加和单点查即可。
// 代码中写的是从后往前,但差不多喵
const int N = 300010;
int n, a[N];
vector<int> p[N];
bool vis[N];
struct BIT {
int tr[N];
void add(int x, int k) {
for (; x <= n + n; x += lowbit(x)) tr[x] += k;
}
int query(int x) {
int res = 0;
for (; x; x -= lowbit(x)) res += tr[x];
return res;
}
} bit;
signed main() {
// freopen("try.in", "r", stdin);
n = read();
for (int i = 1; i <= n + n; i ++)
a[i] = read(), p[a[i] + n].push_back(i), bit.add(i, 1);
int res = 0;
for (int i = n + n; i >= 1; i --) {
if (vis[i]) continue;
p[a[i] + n].pop_back();
int t = p[-a[i] + n].back(); p[-a[i] + n].pop_back();
res += bit.query(i - 1) - bit.query(t) + (a[i] < 0);
bit.add(t, -1);
vis[i] = vis[t] = true;
}
printf("%lld\n", res);
return 0;
}
P11053 [IOI 2024] 马赛克上色
观察会发现,对于所有 i, j \geq 4 的格子的值与 (i - 1, j - 1) 相等,所以我们不妨将整个正方形分为左上角的 4 \times 4,上部的 4 \times n,左部的 n \times 4,以及右下角的部分。前三部分预处理前缀和是容易的,而对于位于右下部的格子的计算可以直接画个图出来,发现直接斜线上的点都相等,分类讨论一下计算即可。
写起来巨麻烦!
#include <bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for (int i = (a); i <= (b); i ++)
#define fro(i, a, b) for (int i = (a); i >= b; i --)
#define INF 0x3f3f3f3f
#define eps 1e-6
#define lowbit(x) (x & (-x))
#define reg register
#define IL inline
typedef long long LL;
typedef std::pair<int, int> PII;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
return x * f;
}
const int N = 500010;
int n, q;
vector<int> x, y, u, d, l, r;
LL b[5][N], c[N][5], cnt1[N], cnt2[N], s1[N], s2[N];
LL getval(int x, int y) {
if (x <= 4) return b[x][y];
if (y <= 4) return c[x][y];
int res = c[x][4] + b[4][y] - b[4][4];
if (x <= y)
return res + (cnt1[x] - cnt1[5]) * (x + 1) - (s1[x] - s1[5]) + (cnt2[y - x + 5] - cnt2[4]) * (x - 4) + (cnt2[y] - cnt2[y - x + 5]) * (y + 1) - (s2[y] - s2[y - x + 5]);
else
return res + (cnt1[x - y + 5] - cnt1[5]) * (y - 4) + (cnt1[x] - cnt1[x - y + 5]) * (x + 1) - (s1[x] - s1[x - y + 5]) + (cnt2[y] - cnt2[4]) * (y + 1) - (s2[y] - s2[4]);
}
vector<LL> mosaic(vector<int> X, vector<int> Y, vector<int> T, vector<int> B, vector<int> L, vector<int> R) {
x = X, y = Y, u = T, d = B, l = L, r = R;
n = X.size(), q = T.size();
for (int i = 0; i < q; i ++) u[i] ++, d[i] ++, l[i] ++, r[i] ++;
map<PII, LL> h;
for (int i = 1; i <= n; i ++) h[{1, i}] = x[i - 1], h[{i, 1}] = y[i - 1];
for (int i = 2; i <= 5; i ++) for (int j = 2; j <= n; j ++) h[{i, j}] = !h[{i, j - 1}] && !h[{i - 1, j}];
for (int i = 2; i <= n; i ++) for (int j = 2; j <= 5; j ++) h[{i, j}] = !h[{i, j - 1}] && !h[{i - 1, j}];
for (int i = 1; i <= 4; i ++) for (int j = 1; j <= n; j ++) b[i][j] = b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1] + h[{i, j}];
for (int i = 1; i <= n; i ++) for (int j = 1; j <= 4; j ++) c[i][j] = c[i - 1][j] + c[i][j - 1] - c[i - 1][j - 1] + h[{i, j}];
for (int i = 5; i <= n; i ++) cnt1[i] = cnt1[i - 1] + h[{i, 5}], s1[i] = s1[i - 1] + h[{i, 5}] * i;
for (int i = 5; i <= n; i ++) cnt2[i] = cnt2[i - 1] + h[{5, i}], s2[i] = s2[i - 1] + h[{5, i}] * i;
vector<LL> ans;
for (int i = 0; i < q; i ++)
ans.push_back(getval(d[i], r[i]) - getval(u[i] - 1, r[i]) - getval(d[i], l[i] - 1) + getval(u[i] - 1, l[i] - 1));
return ans;
}
CF2101B Quartet Swapping
一个显而易见的性质是每次交换都不会改变一个数所在位置的奇偶性,于是“上限”便是按照奇偶分别排序。
但由于每次交换会同时改变奇偶位置,所以我们不一定能够取到上限,比如 1423
。但我们会发现我们一定可以将前 n - 3 个数取到上限,因为从前往后考虑对于 i 位置只要存在 i \sim i + 3 就一定可以让 i 位置取到最优值。
那么我们只需要考虑后三位。由于我们每做一次交换就一定会使得奇数段和偶数段的逆序对 +1 或 -1,所以奇数段和偶数段的逆序对变化量一定同奇偶。我们只要求出取到“上限”时的变化量然后进行微调即可。
Submission #319860206 - Codeforces
P12148 【MX-X11-T2】「蓬莱人形 Round 1」所以我放弃了音乐
其实还是很简单的,但是我第一次写的时候怎么写了个假做法?
最开始想的是从第一行到最后一行找开头然后一直走,后来发现假了,因为无法确定一个格子到底属于哪个开头。但会发现一路跑是不对的,所以考虑从最后一行开始每次往下拓展一格,这样就是对的了。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for (int i = (a); i <= (b); i ++)
#define fro(i, a, b) for (int i = (a); i >= b; i --)
#define INF 0x3f3f3f3f
#define eps 1e-6
#define lowbit(x) (x & (-x))
#define reg register
#define IL inline
typedef long long LL;
typedef std::pair<int, int> PII;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
return x * f;
}
const int N = 1000010;
int n;
PII p[N];
map<int, bool> h[N];
vector<int> g[N];
int main() {
n = read();
for (int i = 1; i <= n; i ++) {
int x = read(), y = read();
g[x].push_back(y); h[x][y] = true;
}
int ans = 0;
for (int i = 1; i <= 1000000; i ++) {
if (!g[i].size()) continue;
sort(g[i].begin(), g[i].end());
for (auto v : g[i])
if (h[i - 1][v - 1]) h[i - 1][v - 1] = false;
else if (h[i - 1][v]) h[i - 1][v] = false;
else if (h[i - 1][v + 1]) h[i - 1][v + 1] = false;
else ans ++;
}
printf("%d\n", ans);
return 0;
}
P12502 「ROI 2025 Day1」天狼星的换班
俄罗斯题目啊。
考虑这其实是一个区间覆盖问题,我们不妨考虑把区间先按照左端点排序,然后考虑区间的合并。
会发现如果两个区间的 m 互相被覆盖,就不能合并(如果互相不覆盖就可以),如果区间 x 和 y 不能拼起来,即 r_x + 1 < l_y,也不能合并。
否则会有两种情况,一种是 m_x 被 y 覆盖,那么画图会发现有 l_y - 1 \leq r_x < m_y;反之,m_y 被 x 覆盖,则 m_x < l_y \leq r_x + 1。
于是我们考虑从前往后合并区间,对于每个 y,寻找前面的线段 x 进行合并。对于第一种情况我们可以直接二分 r_x 的最小值,然后判断下是否 < m_y。对于第二种情况搞点数据结构比如线段树或树状数组进行区间加单点查即可。
可以发现上述过程包含了互相不覆盖的情况。
其实还有个更简单的 DP 做法,注意到两个线段 x, y 能合并当且仅当 r_x \in [l_y - 1, m_y - 1] 或 l_y \in [m_x + 1, r_x + 1]。然后设 f_i 表示按 r 排序后前 i 条线段能否覆盖 [1, r_i]。分别用两棵不同的树状数组维护一下即可。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for (int i = (a); i <= (b); i ++)
#define fro(i, a, b) for (int i = (a); i >= b; i --)
#define INF 0x3f3f3f3f
#define eps 1e-6
#define lowbit(x) (x & (-x))
#define reg register
#define IL inline
typedef long long LL;
typedef std::pair<int, int> PII;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
return x * f;
}
const int N = 500010;
int T, n, k;
set<int> S;
struct Node { int l, m, r; } segs[N];
struct BIT {
int tr[N];
BIT() { for (int i = 1; i <= n + 2; i ++) tr[i] = 0; }
void add(int x, int k) {
for (; x <= n + 2; x += lowbit(x)) tr[x] += k;
}
int query(int x) {
int res = 0;
for (; x; x -= lowbit(x)) res += tr[x];
return res;
}
};
void solve() {
n = read(), k = read();
rep(i, 1, k) segs[i] = {read(), read(), read()};
sort(segs + 1, segs + 1 + k, [&](Node a, Node b) {
return a.l < b.l;
});
S.clear(); BIT tr; int R = 0;
rep(i, 1, k) {
auto it = S.lower_bound(segs[i].l);
if (it != S.end() && (*it) <= segs[i].m || segs[i].l == 1 || tr.query(segs[i].l)) {
R = max(R, segs[i].r);
S.insert(segs[i].r + 1);
tr.add(segs[i].m + 1, 1); tr.add(segs[i].r + 2, -1);
}
}
puts(R == n ? "YES" : "NO");
}
int main() {
T = read();
while (T --) solve();
return 0;
}
````
#### [P12459 [JOI2025 预选赛 R2] 亲密的厨师 ](https://www.luogu.com.cn/problem/P12459)
做过套路的话比较板,那我没做过只能说拼尽全力难以战胜。
考虑预处理出前 $\max{X}$ 大的 $\max(a_i, a_j) + \max(b_i, b_j)$,直接做显然很困难,我们考虑发掘一些单调性。
我们将数组按照 $b$ 排序,会发现 $a_i + \max(b_j, b_{ne_j})$ 是单调的,其中 $ne_j$ 表示下一个可行的决策点。所以我们使用[多路归并优先队列](https://www.luogu.com.cn/problem/P2085)即可维护。
注意对于 $a_i < a_{ne_j}$ 的情况不能计入决策,并且要注意重复计算的问题。
```cpp
#include <bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for (int i = (a); i <= (b); i ++)
#define fro(i, a, b) for (int i = (a); i >= b; i --)
#define INF 0x3f3f3f3f
#define eps 1e-6
#define lowbit(x) (x & (-x))
#define reg register
#define IL inline
typedef long long LL;
typedef std::pair<int, int> PII;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
return x * f;
}
const int N = 400010;
int n, m, q;
int ne[N], to[N], ans[N];
struct Node { int a, b, id; } p[N];
vector<int> Q;
set<int> S[N];
priority_queue<tuple<int, int, int, int>> pq;
void insert(int u) {
int &i = ne[u]; i ++;
for (; i <= n; i ++) {
int j = p[i].id;
if (S[u].count(j)) continue;
j = to[j]; int k = to[u];
if (p[k].a < p[j].a || p[k].a == p[j].a && p[k].id < p[j].id) continue;
pq.push({p[k].a + max(p[j].b, p[k].b), min(u, p[i].id), max(u, p[i].id), u});
return;
}
}
int main() {
n = read(), m = read(), q = read();
rep(i, 1, n) p[i].a = read();
rep(i, 1, n) p[i].b = read(), p[i].id = i, S[i].insert(i);
rep(i, 1, m) {
int x = read(), y = read();
S[x].insert(y); S[y].insert(x);
}
sort(p + 1, p + 1 + n, [&](Node x, Node y) {
return x.b > y.b || x.b == y.b && x.id > y.id;
});
rep(i, 1, n) to[p[i].id] = i;
rep(i, 1, n) insert(i);
rep(i, 1, q) Q.push_back(read());
int R = *max_element(Q.begin(), Q.end());
for (int i = 1; i <= R; i ++) {
auto [v, a, b, u] = pq.top(); pq.pop();
ans[i] = v; insert(u);
}
for (auto x : Q) printf("%d\n", ans[x]);
return 0;
}
P12293 [蓝桥杯 2024 国 Java A] 合并小球
考虑答案可以看作若干段小球的合并,即 ans = \sum{w_{i, j} \times f_{i, j}},其中 s_{i, j} 表示球 i 到球 j 的乘积,f_{i, j} 表示球 i 到球 j 合并并且不与其它球合并的概率。
w_{i, j} 好求,主要看 f_{i, j}。直接做是困难的,因为限制太多了,我们不妨先求出 g_{i, j} 表示球 i 到球 j 合并的概率。然后会发现这个东西也不好求,但我们转而发现数轴长度很短,所以直接求出 p_{i, j} 表示位置 i 的球合并到 j 的概率。
有 p_{i, j} = \frac{1}{4}(p_{i, j} + p_{i + 1, j} + p_{i, j + 1} + p_{i + 1, j + 1}),移项一下可以得到 p_{i, j} = \frac{1}{3}(p_{i + 1, j} + p_{i, j + 1} + p_{i + 1, j + 1})。从而 g_{i, j} = p_{x_i, x_j}。
然后对于 f_{i, j} 我们发现可以做个容斥,有 f_{i, j} = g_{i, j} - g_{i - 1, j} - g_{i, j + 1} + g_{i - 1, j + 1}。这个东西可以理解为类似 j 先合并到 j + 1,i - 1 先合并到 i 然后再一路合并的方案被算重了,所以要加回来。
于是做完了,代码实现时为了避免过多的倒序枚举我们把操作变成终点为 0 然后向左走。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, a, b) for (int i = (a); i <= (b); i ++)
#define fro(i, a, b) for (int i = (a); i >= b; i --)
#define INF 0x3f3f3f3f
#define eps 1e-6
#define lowbit(x) (x & (-x))
#define reg register
#define IL inline
typedef long long LL;
typedef std::pair<int, int> PII;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
while (ch >= '0' && ch <= '9') { x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar(); }
return x * f;
}
const int N = 110, Mod = 998244353;
int n, T, p[N][N], f[N][N], w[N][N];
struct Node { int x, v; } q[N];
IL int mul(int a, int b) { return 1ll * a * b % Mod; }
IL int add(int a, int b) { return ((a + b) % Mod + Mod) % Mod; }
IL int qmi(int a, int b) {
int res = 1;
while (b) {
if (b & 1) res = mul(res, a);
b >>= 1; a = mul(a, a);
}
return res;
}
int main() {
n = read(), T = read();
rep(i, 1, T) p[i][i] = 1;
rep(i, 1, T) rep(j, 1, i - 1) p[i][j] = mul(add(p[i][j - 1], add(p[i - 1][j], p[i - 1][j - 1])), qmi(3, Mod - 2));
rep(i, 1, n) q[i] = {T - read(), read() % Mod};
rep(i, 1, n) rep(j, i, n) f[i][j] = p[q[i].x][q[j].x];
fro(i, n, 1) rep(j, i, n) f[i][j] = add(add(f[i][j], -f[i - 1][j]), add(f[i - 1][j + 1], -f[i][j + 1]));
rep(i, 1, n) rep(j, i, n) {
if (i == j) w[i][j] = q[i].v;
else w[i][j] = mul(w[i][j - 1], q[j].v);
}
int ans = 0;
rep(i, 1, n) rep(j, i, n) ans = add(ans, mul(w[i][j], f[i][j]));
printf("%d\n", ans);
return 0;
}
CF2103D
2000 的构造。
想想想发现对于奇数轮删的数赋 [r - cnt + 1, r] 就好了,偶数轮删的赋 [l, l + cnt - 1],这样各轮间就没有影响了。但是内部的顺序是关键,以奇数轮为例,对于开头的被删数一定比下一个数大,所以是一个递减的;反之,对于结尾来说则是递增的。容易想到以 -1 为分界点,然后做完了。
甚至 WA1 两次。
Submission #320350322 - Codeforces
P11239 [KTSC 2024 R2] 跳跃游戏
不会不会不会喵喵喵啊啊啊www
CF2101C 23 Kingdom
这个题其实有很简单的做法。
考虑对于一个数 x,它的贡献是 \max(x) - \min(x),其中 \max(x) 为 x 最后一次出现的位置,\min(x) 同理。
所以答案类似于 (\max(x) + \max(y) + \max(z)) - (\min(x) + \min(y) + \min(z))。注意到 x, y, z 一定是按照 1, 2, 3, … 的顺序取的,否则不优。
于是我们考虑处理出 pre_k 表示 1 \sim k 需要 a 中的前多少个数才能表示出来,suf_k 同理。计算时考虑贪心,对于每个 a_i 取 \leq a_i 的第一个没取过的数,然后就可以算答案了。
const int N = 200010;
int T, n, a[N], pre[N], suf[N];
void solve() {
n = read();
for (int i = 1; i <= n; i ++) a[i] = read();
for (int i = 1; i <= n; i ++) pre[i] = suf[i] = 0;
set<int> s; int u = 0;
for (int i = 1; i <= n; i ++) s.insert(i);
for (int i = 1; i <= n; i ++) {
auto it = s.upper_bound(a[i]);
if (it == s.begin()) continue;
pre[++ u] = i; s.erase(-- it);
}
u = 0; s.clear();
for (int i = 1; i <= n; i ++) s.insert(i);
for (int i = n; i >= 1; i --) {
auto it = s.upper_bound(a[i]);
if (it == s.begin()) continue;
suf[++ u] = i; s.erase(-- it);
}
LL ans = 0;
for (int i = 1; i <= n; i ++) {
if (pre[i] >= suf[i]) break;
ans += suf[i] - pre[i];
}
printf("%lld\n", ans);
}
int main() {
T = read();
while (T --) solve();
return 0;
}