2026 暑假梦熊青岛集训 B1
2026.7.24 Day6 图论与网络流
部分题目题解:
带权值的同余最短路板子。文章。
类似正常的同余最短路,考虑选取一个基准物品。注意到正常的同余最短路中有要求不会两次经过同一个点,在需要最优化权值的题中,就是选取性价比 \(\frac{c}{v}\) 最大的那个,因为这样如果重复经过一个点,就可以把那 \(V\) 的体积全部换成基准物品,一定不劣。
但是对于一个背包方案 \((V_1,C_1),(V_2,C_2),V_1\equiv V_2\pmod V\),我们该选哪一个?把贡献写出来,应该是 \(C_*-\lfloor\frac{V_q-V*}{V}\rfloor C\),因为 \(V_1,V_2\) 模 \(V\) 同余,所以就相当于最大化 \(C_*-\lfloor\frac{V*}{V}\rfloor C\)。
但是还有一个问题,如果只记录代价的话可能会有 \(V_*>V_q\)。但是注意到这题 \(V_*\) 最优条件下最大只有 \(V^2\le 10^{10}\),而保证 \(V_q\ge 10^{11}\),所以就是合法的。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 50, M = 1e5 + 5;
const int INF = 1e16;
int v[N], c[N], vd[N], vm[N], f[M];
inline void chmax(int &x, int y) { if (x < y) x = y; }
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, q, V = 1, C = 0; cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> v[i] >> c[i];
if (c[i] * V > v[i] * C) V = v[i], C = c[i];
}
for (int i = 1; i <= n; i++)
vd[i] = v[i] / V, vm[i] = v[i] % V;
memset(f, -0x3f, sizeof(f)); f[0] = 0;
for (int i = 1; i <= n; i++)
for (int j = __gcd(v[i], V) - 1; ~j; j--)
for (int _ = 0, k = j; _ < 2; ) {
int t = k + vm[i], s = vd[i];
if (t >= V) t -= V, s++;
chmax(f[t], f[k] + c[i] - s * C);
k = t; if (k == j) _++;
}
while (q--) {
int x; cin >> x; int y = x % V;
if (f[y] < -INF) cout << "-1\n";
else cout << f[y] + x / V * C << '\n';
}
return 0;
}
经典模型,需要积累。
切糕模型,形如对若干个变量之间的取值大小关系有限制,最小割。
对于每个 \(X_i\),建一条从 \(S\) 到 \(T\) 长度为 \(V+1\) 的链 \(S\xrightarrow{0}1\xrightarrow{1}2\xrightarrow{2}\cdots V\xrightarrow{V} T\),而对 \(Y_i\) 建立反过来的链 \(S\xrightarrow{V}V\xrightarrow{V - 1}V - 1\xrightarrow{V - 2}\cdots 1\xrightarrow{0} T\),割掉权值为 \(a\) 的边相当于 \(X_i/Y_i=a\)。
初始有 \(X_i\) 的限制,那么就是第 \(i\) 条链上 \(S\) 向 \(X_i\) 连权值为 \(+\infty\) 的边,\(Y\) 同理;对于那 \(m\) 个限制,要求要么 \(\forall j,X_j\ge a_{i,j}\) 或 \(\forall j,Y_j\ge a_{i,j}\),对这个限制建一个虚拟点,然后虚拟点向 \(X\) 链上对应权值 \(a\) 的点连边、\(Y\) 链上对应权值 \(a\) 的点向虚拟点连边即可。
对于是否需要对链连 \(+\infty\) 的反向边,这题权值是递增的所以不需要(割多条边一定不劣),但是某些更一般的情况是需要的,详见这篇文章。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5, M = 6e6 + 5, V = 500;
const int INF = 1e9;
struct edge { int nxt, to, w; } e[M];
int head[N], ecnt = 1, S, T, _head[N];
inline void add(int u, int v, int w) {
e[++ecnt] = {head[u], v, w}, head[u] = ecnt;
e[++ecnt] = {head[v], u, 0}, head[v] = ecnt;
}
int dis[N];
queue <int> q;
inline bool bfs() {
memset(dis, 0, sizeof(dis));
q.push(S), dis[S] = 1;
while (q.size()) {
int t = q.front(); q.pop();
for (int i = head[t]; i; i = e[i].nxt)
if (!dis[e[i].to] && e[i].w)
q.push(e[i].to), dis[e[i].to] = dis[t] + 1;
}
return dis[T];
}
int dfs(int now, int flow) {
if (now == T) return flow; int sum = flow;
for (int &i = _head[now]; i; i = e[i].nxt)
if (dis[e[i].to] == dis[now] + 1 && e[i].w) {
int t = dfs(e[i].to, min(e[i].w, flow));
e[i].w -= t, e[i ^ 1].w += t, flow -= t;
if (!flow) break;
}
if (sum == flow) dis[now] = 0;
return sum - flow;
}
inline int dinic() {
int ans = 0;
while (bfs()) {
memcpy(_head, head, sizeof(_head));
ans += dfs(S, 1e18);
}
return ans;
}
int n, m, idx[505][505], idy[505][505];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int ts = 0, n, m; cin >> n >> m; S = ++ts, T = ++ts;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= V; j++) idx[i][j] = ++ts;
add(S, idx[i][1], 0), add(idx[i][V], T, V);
for (int j = 1; j <= V; j++) add(idx[i][j], idx[i][j + 1], j);
for (int j = 1; j <= V; j++) idy[i][j] = ++ts;
add(S, idy[i][V], V), add(idy[i][1], T, 0);
for (int j = 1; j < V; j++) add(idy[i][j + 1], idy[i][j], j);
}
for (int i = 1, x; i <= n; i++)
cin >> x, add(S, idx[i][x], INF);
for (int i = 1, y; i <= n; i++)
cin >> y, add(idy[i][y], T, INF);
for (int i = 1; i <= m; i++) {
int id = ++ts;
for (int j = 1; j <= n; j++) {
int x; cin >> x;
add(id, idx[j][x], INF), add(idy[j][x], id, INF);
}
}
cout << dinic() << '\n';
return 0;
}
*ARC129E Yet Another Minimization
转化很厉害。
类似上一题,先将每个变量的取值链建出来。
然后考虑如何刻画需要额外花费的代价。不妨设 \(x_i\le x_j\),发现如果以链状结构来刻画的画刻画的是变量 \(\le a\) 或 \(\ge b\),而无法刻画单点 \(=a\)。因此联想到拆贡献,将 \(x_i-x_j\) 拆成 \(x_i-(x_i-1)+(x_i-1)-(x_i-2)+\cdots -x_j\),那么就是对于每个 \(k\),将 \((j,k)\) 向 \((i,k)\) 连权值为 \(w_{i,j}\) 的边,\(x_i>x_j\) 同理。
但是值域有 \(10^6\),根本过不去。每个点有用的取值只有 \(5\) 个,\(i,j\) 的取值可以被划分为至多 \(10\) 个等价的段(按 \(a_{i/j,*}\) 从小到大排序得到 \(b\),那么 \([b_i,b_{i+1})\) 内的点是等价的),限制变成 \(x_j\ge b_{i+1},x_i\le b_i\),权值变成 \((b_{i+1} - b_i)w_{i,j}\)。
此时点数变为 \(O(nm)\),边数变为 \(O(n^2m)\),轻松通过。
这道题和切糕那道题类似,分析一下可以得到不用建反向边也是正确的。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2005, M = 2e6 + 5;
const int INF = 1e18;
struct edge { int nxt, to, w; } e[M];
int head[N], ecnt = 1, S, T, _head[N];
inline void add(int u, int v, int w) {
e[++ecnt] = {head[u], v, w}, head[u] = ecnt;
e[++ecnt] = {head[v], u, 0}, head[v] = ecnt;
}
int dis[N];
queue <int> q;
inline bool bfs() {
memset(dis, 0, sizeof(dis));
q.push(S), dis[S] = 1;
while (q.size()) {
int t = q.front(); q.pop();
for (int i = head[t]; i; i = e[i].nxt)
if (!dis[e[i].to] && e[i].w)
q.push(e[i].to), dis[e[i].to] = dis[t] + 1;
}
return dis[T];
}
int dfs(int now, int flow) {
if (now == T) return flow; int sum = flow;
for (int &i = _head[now]; i; i = e[i].nxt)
if (dis[e[i].to] == dis[now] + 1 && e[i].w) {
int t = dfs(e[i].to, min(e[i].w, flow));
e[i].w -= t, e[i ^ 1].w += t, flow -= t;
if (!flow) break;
}
if (sum == flow) dis[now] = 0;
return sum - flow;
}
inline int dinic() {
int ans = 0;
while (bfs()) {
memcpy(_head, head, sizeof(_head));
ans += dfs(S, 1e18);
}
return ans;
}
int a[N][N], c[N][N], w[N][N], id[N][N], b[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m, ts = 0; cin >> n >> m;
S = ++ts, T = ++ts;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> a[i][j] >> c[i][j];
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++) cin >> w[i][j];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) id[i][j] = ++ts;
add(id[i][m], T, INF), id[i][0] = S;
for (int j = 1; j <= m; j++)
add(id[i][j - 1], id[i][j], c[i][j]);
}
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++) {
int ts = 0;
for (int k = 1; k <= m; k++)
b[++ts] = a[i][k], b[++ts] = a[j][k];
sort(b + 1, b + 1 + ts);
ts = unique(b + 1, b + 1 + ts) - b - 1;
for (int k = 1; k < ts; k++) {
int x = lower_bound(a[i] + 1, a[i] + m + 1, b[k + 1]) - a[i] - 1;
int y = lower_bound(a[j] + 1, a[j] + m + 1, b[k + 1]) - a[j] - 1;
add(id[i][x], id[j][y], (b[k + 1] - b[k]) * w[i][j]);
add(id[j][y], id[i][x], (b[k + 1] - b[k]) * w[i][j]);
}
}
cout << dinic() << '\n';
return 0;
}
虽然算是半个二分图边染色的板子,但还是很妙。
先考虑 \(C=n\) 怎么做,即每一列能填哪些数已经确定了,要求那 \(n-R\) 行每一行中不重复。
将每一列作为左部点,每个数作为右部点,如果第 \(i\) 列可以填 \(j\) 则左部点 \(i\) 向右部点 \(j\) 连边,每一行的方案数就是一个完美匹配,而这个图是一个 \(n-R\) 的正则二分图,所以一定有解,可以用二分图边染色的方法求出每一组解。
二分图边染色(要求有公共端点的两条边颜色不同)
首先有结论:最小染色数等于点度数最大值,必要性显然,下用构造证充分性。
依次加入边 \((u,v)\),尽可能给你分配小的颜色。设 \(i\) 点连的边还能使用的颜色集合为 \(S_i\),令 \(x=\operatorname{mex}(S_u),y=\operatorname{mex}(S_v)\),若 \(x=y\) 则万事大吉,否则不妨设 \(x<y\),强制更改其颜色为 \(x\),考虑冲突的 \((v,w)\) 边,尝试将其强制改为 \(y\),再冲突就再把 \((w,p)\) 改为 \(x\),以此类推直到不矛盾。
贺一张题解中的图:
那么我们就会一路走下去到 A 并修改一路上经过边的颜色。
由于更改的颜色序列形如 \((x,y,x,y\cdots)\),二分图无奇环,因此最终一定有解。
复杂度 \(O((a+b)m)\)。
然后回到普遍情况,当 \(C=n\) 后是一定有解的,因此我们只需要把 \(x\le R,y>C\) 的部分填完就做完了。在此尝试这样做一遍,但是此时其实并不是一个正则二分图,左部点度数均为 \(n-C\),但是右部点不一定,此时对应了一个不合法的情况,若右部点度数 \(>n-C\) 那么一定无解,否则边染色即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int deg[N], a[N][N];
struct Graph {
int b[N << 1][N];
inline void init() {
memset(b, 0, sizeof(b));
}
void work(int u, int v) {
int x = 1, y = 1;
while (b[u][x]) x++; while (b[v][y]) y++;
b[u][x] = v, b[v][y] = u;
if (x ^ y) {
for (int i = v, c = y; i; i = b[i][c], c ^= x ^ y)
swap(b[i][x], b[i][y]);
}
}
} G;
bool vis[N];
inline void solve() {
int n, R, C; cin >> n >> R >> C;
for (int i = 1; i <= n; i++) deg[i] = R;
for (int i = 1; i <= R; i++)
for (int j = 1; j <= C; j++)
cin >> a[i][j], deg[a[i][j]]--;
for (int i = 1; i <= n; i++)
if (deg[i] > n - C) return cout << "No\n", void();
G.init();
for (int i = 1; i <= R; i++) {
memset(vis, 0, sizeof(vis));
for (int j = 1; j <= C; j++) vis[a[i][j]] = true;
for (int j = 1; j <= n; j++)
if (!vis[j]) G.work(i, j + n);
}
for (int i = 1; i <= R; i++)
for (int j = C + 1; j <= n; j++)
a[i][j] = G.b[i][j - C] - n;
G.init();
for (int j = 1; j <= n; j++) {
memset(vis, 0, sizeof(vis));
for (int i = 1; i <= R; i++) vis[a[i][j]] = true;
for (int i = 1; i <= n; i++)
if (!vis[i]) G.work(j, i + n);
}
for (int i = R + 1; i <= n; i++)
for (int j = 1; j <= n; j++)
a[i][j] = G.b[j][i - R] - n;
cout << "Yes\n";
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
cout << a[i][j] << ' ';
cout << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T--) solve();
return 0;
}
*P9542 [湖北省选模拟 2023] 棋圣 / alphago
考验功底的题。
先尝试一些简单的结构手玩一下,如果你很不幸挑到了链,那么恭喜你——
正常人看部分分应该是尝试环。环相当于每个点可以往任意一边走。很自由啊,会不会直接能取到最优?最优即最大边两边一边是所有白点,一边是所有黑点。手玩一下,若最终操作的次数为奇数,那么每个点应该走奇数步到目标点然后往返,否则是偶数步。如果是奇环的话,两条路径一条奇一条偶,因此可以取到最优情况。但是偶环不行,注意到一次操作两点距离奇偶性不会改变,因此将环黑白染色,相同颜色的点一定到一起,所以答案是 \(mxW(c_{b,0}c_{w,1}+c_{b,1}c_{w,0})\)。
然后继续看树。首先肯定它不优于偶环,看是否能等于。操作一次使得叶子结点空出来,然后不断轮流操作叶子结点,存在“分岔”时两点间距离就会不断变小,和偶环的情况是一样的。
注意没有分岔,即链的情况复杂一些,因为可能存在连续的一些点而无法使他们合并。但是链的情况又是简单的,操作比较简单,可以直接刻画怎样的终局是合法的:点的顺序不改变;相邻点距离不变大、奇偶性不变。这就是一个比较简单的序列 dp,设 \(f_{i,l,r}\) 表示第 \(i\) 个点放 \([l,r]\) 棋子最大的权值,注意到转移枚举上一个放棋子的点 \(j\) 的复杂度之和是 \(\sum pos_i-pos_{i-1}\)(距离不增),所以总复杂度是 \(O(n^4)\)。还可以优化到 \(O(n^3)\),但懒了。
和这题有点像的一题是 P10367,都是讨论各种各样的图,并且都用到了在利用奇环转化奇偶性的思想(更普遍地,其实是将图分解为特殊结构+附加边的思想)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 105;
struct node { int to, w; };
vector <node> p[N];
int a[N], co[N], cnt[2][2], pos[N], w[N], W[N];
bool F, vis[N];
void dfs(int k, int c) {
if (~co[k]) {
if (c ^ co[k]) F = 1;
return;
} else co[k] = c;
if (~a[k]) cnt[c][a[k]]++;
for (auto i : p[k]) dfs(i.to, c ^ 1);
}
int f[N][N][N], c0[N], c1[N];
inline int get0(int l, int r) { return c0[r] - c0[l - 1]; }
inline int get1(int l, int r) { return c1[r] - c1[l - 1]; }
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(a, -1, sizeof(a));
int n, m, k; cin >> n >> m >> k;
for (int i = 1, x, c; i <= k; i++)
cin >> x >> c, a[x] = c;
int mx = 0;
for (int i = 1; i <= m; i++) {
int u, v, w; cin >> u >> v >> w;
p[u].push_back({v, w});
p[v].push_back({u, w});
mx = max(mx, w);
}
bool fl = 0;
for (int i = 1; i <= n; i++) fl |= p[i].size() == 1;
for (int i = 1; i <= n; i++) fl &= p[i].size() <= 2;
if (!fl) {
memset(co, -1, sizeof(co)), dfs(1, 0);
if (F) cout << (cnt[0][0] + cnt[1][0]) * (cnt[0][1] + cnt[1][1]) * mx << '\n';
else cout << (cnt[0][0] * cnt[1][1] + cnt[0][1] * cnt[1][0]) * mx << '\n';
return 0;
}
int now = 1, ts = 0;
for (int i = 1; i <= n; i++)
if (p[i].size() == 1) now = i;
for (int _ = 1; _ <= n; _++) {
vis[now] = true;
if (~a[now]) pos[++ts] = _, w[ts] = a[now];
for (auto j : p[now])
if (!vis[j.to]) now = j.to, W[_ + 1] = j.w;
}
for (int i = 1; i <= ts; i++) {
c0[i] = c0[i - 1] + (w[i] == 0);
c1[i] = c1[i - 1] + (w[i] == 1);
}
memset(f, -0x3f, sizeof(f));
for (int i = 1; i <= n; i++)
for (int l = 1; l <= ts; l++)
for (int r = l; r <= ts; r++) {
if (pos[r] - pos[l] & 1) break;
if (l == 1) { f[i][l][r] = 0; continue; }
for (int k = i - 1; k >= i - (pos[l] - pos[l - 1]) && k; k--) {
if (k && (i - k & 1) != (pos[l] - pos[l - 1] & 1)) continue;
for (int x = 1; x <= l; x++) {
int w = 0;
if (k == i - 1)
w = (get0(l, r) * get1(x, l - 1) + get1(l, r) * get0(x, l - 1)) * W[i];
f[i][l][r] = max(f[i][l][r], f[k][x][l - 1] + w);
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= ts; j++) ans = max(ans, f[i][j][ts]);
cout << ans << '\n';
return 0;
}
很厉害的题目。
首先选的点肯定是一个联通块,进一步的,如果 \((u,v)\) 被选了,那么 \(u\) 到 \(v\) 路径上的所有点都会被选。只选一个点不合法,其余情况都形如选若干个相邻的点双。
建出圆方树,发现相当于直接删掉圆方树上的方点,要求删的方点隔一个圆点相邻。设 \(siz_i\) 为子树内圆点个数,\(k=0\) 比较简单,枚举联通块大小 \(d\) 对于一个圆点,如果 \(d\mid siz\) 则父亲的方点一定要被删,最后再 chk 一下是否合法即可。
然后是 \(siz=1\),枚举联通块大小为 \(d\) 或 \(d+1\)。发现其实没有很好的办法来刻画方点是否连通,因为不在一个子树内不好判断,不妨尝试转化为更好判断的条件。取带权重心为根,若其为方点,则必须被选,否则其所在联通块 \(>\frac{n}{2}\),一定不合法;而若其为圆点,儿子方点中至少有一个被选,那么限制就变成了一个方点能选当且仅当其父亲方点被选了。这样的好处是将联通块型限制转化为祖先 - 儿子型限制,利于树形 dp 等的进行,一个考虑局势取重心。
设 \(f_i\) 表示若 \(i\) 为圆点,删去其父亲方点后子树内均满足联通块位 \(d,d+1\) 的方案数;若 \(i\) 为方点,则为其所有儿子圆点的 \(f\) 乘积之和。转移只需要考虑前者,手玩一下,情况其实很少(设当前要从 \(i\) 的儿子 \(j\) 转移到 \(i\)):
- 若 \(siz_j<d\),则子树内不删;
- 若 \(siz_j>d\),则子树内一定要删;
- 若 \(siz_j=d\),则最多可以选一个这样的儿子和 \(i\) 待一块,但如果 \(f_j=0\) 的话就一定只能选它了。
具体转移不难想。此时还是 \(O(n^2)\) 的,因为要枚举 \(d\)。但是观察到 \(d\) 能合法的必要条件是 \(\lceil{\frac{n}{d+1}}\rceil\le \lfloor\frac{n}{d}\rfloor\),这样的 \(d\) 取值只有 \(O(\sqrt n)\),于是总的复杂度就是 \(O(n\sqrt n)\) 的了。
实现时需要一定的剪枝,比如若存在 \(siz_i>k\) 且 \(f_i=0\) 则 \(d\) 一定不合法,直接退出。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
const int P = 998244353;
inline int mod(int x) { return x >= P ? x - P : x; }
inline void add(int &x, int y) { x = mod(x + y); }
vector <int> g[N], p[N];
int dfn[N], low[N], ts, cnt;
stack <int> q;
void tarjan(int k) {
dfn[k] = low[k] = ++ts, q.push(k);
for (auto i : g[k]) {
if (!dfn[i]) {
tarjan(i), low[k] = min(low[k], low[i]);
if (low[i] >= dfn[k]) {
int t; cnt++;
do {
t = q.top(), q.pop();
p[t].push_back(cnt);
p[cnt].push_back(t);
} while (t ^ i);
p[k].push_back(cnt), p[cnt].push_back(k);
}
} else low[k] = min(low[k], dfn[i]);
}
}
int n, rt, mx[N], siz[N], F[N];
void findrt(int k, int fa) {
siz[k] = k <= n, F[k] = fa;
for (auto i : p[k]) {
if (i == fa) continue;
findrt(i, k), siz[k] += siz[i];
mx[k] = max(mx[k], siz[i]);
}
mx[k] = max(mx[k], n - siz[k]);
if (!rt || mx[k] < mx[rt]) rt = k;
}
void dfs0(int k, int fa) {
siz[k] = k <= n, F[k] = fa;
for (auto i : p[k])
if (i ^ fa) dfs0(i, k), siz[k] += siz[i];
}
namespace solve1 {
bool fl, vis[N];
void dfs(int k, int fa, int op) {
if (k > n && vis[k] && !op) return fl = 0, void();
if (k > n && !vis[k]) op = 0;
for (auto i : p[k])
if (i ^ fa) dfs(i, k, op);
}
int siz[N], fa[N];
inline int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
inline void merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return;
fa[y] = x, siz[x] += siz[y];
}
inline bool chk(int d) {
memset(vis, 0, sizeof(vis)); fl = 0;
for (int i = 1; i <= n; i++)
if (::siz[i] % d == 0) vis[F[i]] = true, fl = 1;
dfs(rt, 0, 1); if (!fl) return 0;
for (int i = 1; i < 2 * n; i++) siz[i] = i <= n, fa[i] = i;
for (int i = 1; i <= n; i++)
for (auto j : p[i])
if (!vis[j]) merge(i, j);
for (int i = 1; i <= n; i++)
if (siz[find(i)] ^ d) return 0;
return 1;
}
}
namespace solve2 {
int f[N];
bool fl;
void dfs(int k, int fa, int d) {
if (fl) return ;
f[k] = 0; if (siz[k] < d) return;
if (k > n) {
f[k] = 1;
for (auto i : p[k]) {
if (i == fa) continue; dfs(i, k, d);
f[k] = 1ll * f[k] * f[i] % P;
if (!f[k]) break;
}
} else {
int cnt = 1, s = 1, tmp = 0;
for (auto i : p[k]) {
if (i == fa) continue; dfs(i, k, d);
if (siz[i] < d) cnt += siz[i];
else if (siz[i] == d)
f[i] ? ++tmp : cnt += d;
else s = 1ll * s * f[i] % P;
if (!s) break;
}
if (d <= cnt && cnt <= d + 1) f[k] = s;
if (cnt == 1) f[k] = (f[k] + 1ll * s * tmp) % P;
}
if (siz[k] > d + 1 && !f[k]) fl = 1;
}
inline int cal(int d) {
fl = 0; dfs(rt, 0, d); if (fl) return 0;
return f[rt] - solve1::chk(d + 1) ;
}
}
bool vis[N];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int m, k; cin >> n >> m >> k;
for (int i = 1; i <= m; i++) {
int u, v; cin >> u >> v;
g[u].push_back(v), g[v].push_back(u);
}
cnt = n; tarjan(1), findrt(1, 0), dfs0(rt, 0);
if (!k) {
int ans = 0;
for (int i = 1; i * i <= n; i++)
if (n % i == 0) {
ans += solve1::chk(i);
if (i * i != n && i ^ 1) ans += solve1::chk(n / i);
}
cout << ans << '\n';
return 0;
}
int ans = 0;
for (int i = 2; i <= n; i++) {
vis[n / i] = true;
if (n % i == 0) vis[n / i - 1] = true;
}
for (int i = 1; i <= n; i++)
if (vis[i]) add(ans, solve2::cal(i));
cout << (ans + P) % P;
return 0;
}
感觉非正解更牛的一集。
第一反应是 \(v\) 对 \(f(u,G)\) 有贡献当且仅当仅经过 \([v,n]\) 内的点 \(u\) 可以到 \(v\) 且 \(v\) 可以到 \(u\),问题在于某些不符合的 \(v'\) 对 \(v\) 没有限制,但仔细思考一下可以反证 \(v'\) 对 \(v\) 是否有贡献没有影响。
答案还要对于加边的后缀都求一遍这个东西,发现 \((u,v)\) 合法的是一个前缀,所以可以求出所有 \((u,v)\) 合法需要经过的最小编号的边,然后差分+前缀和一下算出所有的答案。
显然有 \(v<u\),而经过不超过 \(v\) 的点到达,路径上经过的点联想到 floyd,从大到小枚举中转点跑即可。复杂度是 \(O(n^3)\) 的,需要一定的剪枝可以跑过去。
#include <bits/stdc++.h>
using namespace std;
const int N = 1005, M = 2e5 + 5;
int f[N][N], ans[M];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m;
for (int i = 1, x, y; i <= m; i++)
cin >> x >> y, f[x][y] = i;
for (int k = n; k; k--)
for (int i = 1; i <= n; i++) {
if (!f[i][k]) continue;
for (int j = (i <= k ? n : k - 1); j; j--)
f[i][j] = max(f[i][j], min(f[i][k], f[k][j]));
}
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
ans[min(f[i][j], f[j][i])]++;
ans[m + 1] += n;
for (int i = m; i; i--) ans[i] += ans[i + 1];
for (int i = 1; i <= m + 1; i++) cout << ans[i] << ' ';
return 0;
}
2026.7.25 省选模拟赛 1
题面。
有点棒棒糖,但是硬控我 1.5h+。
两种做法。第一种是注意到 \(mx\) 的左边贡献为 \(\sum l_p\),右边为 \(\sum r_p\),可以直接上单侧递归线段树做到 \(O(n\log^2n)\),也可以精细实现线段树二分+区间覆盖做到 \(O(n\log n)\)。
另一种需要一个妙妙转化,就是 \(\min(l_p,r_p)=l_p+r_p-\max(l_p,r_p)=l_p+r_p-mx\),是 min-max 容斥最简单的运用,谨记 \(\max\) 和 \(\min\) 一定程度上可以转化,然后直接上单调栈+启发式合并即可,是单 \(\log\) 的。
我写的是单侧递归,虽然多个 \(\log\) 但这个可以解决任意 \(l,r\) 的查询。
#include <bits/stdc++.h>
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
#define int long long
using namespace std;
const int N = 1e6 + 5;
int a[N];
struct Segment {
struct node {
int lw, rw, mx;
} t[N];
int getl(int k, int l, int r, int w) {
if (l == r) return max(w, t[k].lw) ; int m = l + r >> 1;
if (w >= t[ls(k)].mx) return w * (m - l + 1) + getl(rs(k), m + 1, r, w);
else return t[k].lw - t[ls(k)].lw + getl(ls(k), l, m, w);
}
int getr(int k, int l, int r, int w) {
if (l == r) return max(w, t[k].rw); int m = l + r >> 1;
if (w >= t[rs(k)].mx) return w * (r - m) + getr(ls(k), l, m, w);
else return t[k].rw - t[rs(k)].rw + getr(rs(k), m + 1, r, w);
}
inline void pushup(int k, int l, int r) {
t[k].mx = max(t[ls(k)].mx, t[rs(k)].mx);
int m = l + r >> 1;
t[k].lw = t[ls(k)].lw + getl(rs(k), m + 1, r, t[ls(k)].mx);
t[k].rw = getr(ls(k), l, m, t[rs(k)].mx) + t[rs(k)].rw;
}
void build(int k, int l, int r) {
if (l == r) {
int x; cin >> x; a[l] = x;
return t[k] = {x, x, x}, void();
}
int m = l + r >> 1;
build(ls(k), l, m), build(rs(k), m + 1, r);
pushup(k, l, r);
}
int w;
int queryl(int k, int l, int r, int L, int R) {
if (L <= l && r <= R) {
int res = getl(k, l, r, w); w = max(w, t[k].mx);
return res;
}
int m = l + r >> 1, res = 0;
if (L <= m) res = queryl(ls(k), l, m, L, R);
if (R > m) res += queryl(rs(k), m + 1, r, L, R);
return res;
}
int queryr(int k, int l, int r, int L, int R) {
if (L <= l && r <= R) {
int res = getr(k, l, r, w); w = max(w, t[k].mx);
return res;
}
int m = l + r >> 1, res = 0;
if (R > m) res = queryr(rs(k), m + 1, r, L, R);
if (L <= m) res += queryr(ls(k), l, m, L, R);
return res;
}
} T;
int n, l[N], r[N], pos[N], s[N];
struct BIT {
int c[N];
inline void add(int x, int y) {
for (; x < N; x += x & -x) c[x] += y;
}
inline int qry(int x) {
int res = 0;
for (int i = 20; ~i; i--) {
int j = 1 << i;
if (res + j <= n && x > c[res + j])
res += j, x -= c[res];
}
return res + 1;
}
} B;
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n; T.build(1, 1, n);
for (int i = 1; i <= n; i++)
l[i] = r[i] = pos[i] = i, s[i] = a[i], B.add(i, 1);
for (int i = 1; i < n; i++) {
int x; cin >> x; int p = B.qry(x), q = B.qry(x + 1);
int y = a[pos[p]] >= a[pos[q]] ? pos[p] : pos[q];
T.w = 0; int res = T.queryl(1, 1, n, l[p], y);
if (y < r[q]) T.w = 0, res += T.queryr(1, 1, n, y + 1, r[q]);
cout << res - s[p] - s[q] << '\n';
s[p] += s[q], r[p] = r[q], pos[p] = y, B.add(q, -1);
}
return 0;
}

\(\text{64MB},1s\)。\(1\le n,q\le 1.2\times 10^5,1\le c_i\le 10^9\)。
暴力是容易的,找出所有合法的点然后前面能选就尽量选。
但是注意到这是一个依赖前面的暴力,不好直接算,需要寻找直接刻画的形式。 套路性地运用最大匹配 = 最小点覆盖,或手玩一下可以得到:相邻两个数必须选一个,选出的数的最小权值和。
注意到在最开始加点是不好做的,所以线段树啥的不太行。但是这写出 dp,本质上可以写成一个 \((\min,+)\) 的矩阵乘法(这也对应了为什么前面加点不好做,因为矩阵乘法没有交换律,因此如果前面加信息不好做的或许可以考虑矩阵乘法)。
按下标分块。散块是容易的,整块需要预处理出所有 \(a_i\in(x,y)\) 的矩阵之积。考虑块内继续分治处理,一个长度为 \(k\) 的区间有用的 \((x,y)\) 只有 \(k^2\) 对,左右合并是 \(O(1)\) 的,于是复杂度就是 \(T(k)=2T(\frac{2}{k})+O(k^2)=O(k^2)\)。
取块长 \(\sqrt n\) 即可做到 \(O(n\sqrt n)\)。实现时有卡空间,因此需要对块扫描线,做完一个块就扔掉一个块的信息,这样空间就是 \(O(n)\) 的了。
看上去很史不想写。trick 似乎来自P7721 [Ynoi2007] rvrewsus。
结论:记一个坐标的权值为各维坐标之和,那么答案为出现最多的权值的出现次数,同时这个权值应等于 \(\lfloor\frac{\sum_{i=1}^na_i+1}{2}\rfloor\)。如果有深厚的数学功底可以尝试证明,但我猜也猜出来了。
然后问题就变成了求 \(\sum x_i=M,1\le x_i\le p_i\) 的方案数。典的是容斥一下变成 \(\sum_S(-1)^{|S|}\dbinom{M-1-sum(S)}{n-1}\),其中 \(S\in\{1,2,\cdots n\},sum(S)=\sum_{i\in S} p_i\),要求 \(sum(S)\le M - n\)。
这个枚举 \(S\) 是 \(O(2^n)\) 的,那么不难想到折半。关键是如何将组合数拆掉。范德蒙德卷积可以来拆组合数:
需要利用广义二项式系数定义:\(\dbinom{n}{m}=\frac{\prod_{i=n-m+1}^n i}{m!}\)。
合并两边答案双指针以下就行了(注意对 \(sum(S)\) 有限制),复杂度 \(O(2^\frac{n}{2}n^2)\)。
#include <bits/stdc++.h>
#define pii pair <int, int>
#define fi first
#define se second
#define int long long
using namespace std;
const int N = 1e6 + 5;
const int P = 1e9 + 7;
inline int qpow(int a, int b = P - 2) {
int res = 1;
while (b) {
if (b & 1) res = res * a % P;
a = a * a % P, b >>= 1;
}
return res;
}
int a[N], tb, tc, inv[N], wb[N], wc[N];
pii b[N], c[N];
void dfs1(int k, int lim, int z, int s) {
if (k > lim) return b[++tb] = {s, z}, void();
dfs1(k + 1, lim, z, s), dfs1(k + 1, lim, z ^ 1, s + a[k]);
}
void dfs2(int k, int lim, int z, int s) {
if (k > lim) return c[++tc] = {s, z}, void();
dfs2(k + 1, lim, z, s), dfs2(k + 1, lim, z ^ 1, s + a[k]);
}
inline int cal(int n, int m) {
int res = 1;
for (int i = n; i > n - m; i--) res = i % P * res % P;
return res;
}
inline int C(int n, int m) {
return cal(n, m) * inv[m] % P;
}
signed main() {
int T; cin >> T;
while (T--) {
int n, M = 0; cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i], M += a[i] + 1;
M >>= 1; dfs1(1, n / 2, 0, 0); dfs2(n / 2 + 1, n, 0, 0);
sort(b + 1, b + 1 + tb), sort(c + 1, c + 1 + tc);
inv[0] = 1;
for (int i = 1, w = 1; i <= n; i++)
w = w * i % P, inv[i] = qpow(w);
int ans = 0;
for (int k = 0; k < n; k++) {
for (int i = 1; i <= tb; i++) {
int w = C(M - 1 - b[i].fi, k);
wb[i] = b[i].se ? P - w : w;
}
for (int i = 1; i <= tc; i++) {
int w = C(-c[i].fi, n - 1 - k);
wc[i] = c[i].se ? P - w : w;
}
for (int i = tb, j = 0, s = 0; i; i--) {
if (b[i].fi > M - n) continue;
while (j < tc && c[j + 1].fi + b[i].fi <= M - n)
j++, s = (s + wc[j]) % P;
ans = (ans + wb[i] * s) % P;
}
}
cout << (ans % P + P) % P <<'\n'; tb = tc = 0;
}
return 0;
}
2026.7.26 复杂字符串
纯粹的字符串题算得上好题,字符串结合 ds 可能偏 educational。
部分题目题解:
很好的题目,感觉第一次见的话难自己做出来。
相当于对于所有是 \(S\) 的周期的长度做一个背包,看能凑出多少个 \(\le W\) 的和。
但是 \(W\) 非常大 + 可行性背包,所以考虑同余最短路。
每一个可能的长度都跑一遍显然会超时,但是注意到 border 理论中周期的性质:可以被划分为 \(O(\log n)\) 个等差数列,考虑对于每个等差数列一起做。
对于一个 \((st,len,d)\) 的等差数列,以 \(st\) 作背包的基准值,那么转移就是
把正常按 \(d\) 的每个转移的环找出来,从最小值的地方断开(跨过最小值直接取最小值最优),那么环上的每个位置都可以从前面 \(len\) 个位置转移来,容易用单调队列优化 dp 做到单次 \(O(n)\)。
还有一个问题,不止一个等差数列,每次需要转化一下基准值。但也很简单,这就相当于先把原来的 \(f\) 扔到对应模新 \(st\) 的位置,然后将原先的 \(st'\) 视作一个物品更新一遍即可。
看似很复杂,复杂度就是简单的 \(O(Tn\log n)\),并且实测根本不卡常。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 5;
char a[N];
int fail[N], bd[N], f[N], P, g[N];
inline void change(int m) {
memcpy(g, f, sizeof(g)), memset(f, 0x3f, sizeof(f));
for (int i = 0; i < m; i++)
f[g[i] % m] = min(f[g[i] % m], g[i]);
for (int i = 0; i < __gcd(P, m); i++)
for (int j = i, _ = 0; _ < 2; _ += j == i) {
int t = (j + P) % m;
f[t] = min(f[t], f[j] + P), j = t;
}
P = m;
}
int b[N], q[N], w[N];
inline void work(int a, int len, int d) {
change(a); if (!len) return;
for (int st = 0; st < __gcd(a, d); st++) {
int mn = st, now = (st + d) % a;
while (now ^ st) {
if (f[now] < f[mn]) mn = now;
now = (now + d) % a;
}
now = mn; int ts = 0;
do {
b[++ts] = now, now = (now + d) % a;
} while (now ^ mn);
int head = 1, tail = 0;
for (int i = 1; i <= ts; i++) {
while (head <= tail && q[head] < i - len) head++;
if (head <= tail)
f[b[i]] = min(f[b[i]], w[head] + i * d + a);
while (head <= tail && w[tail] > f[b[i]] - i * d) tail--;
q[++tail] = i, w[tail] = f[b[i]] - i * d;
}
}
}
inline void solve() {
int n, W; cin >> n >> W;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 2; i <= n; i++) {
int j = fail[i - 1];
while (j && a[i] != a[j + 1]) j = fail[j];
fail[i] = j + (a[i] == a[j + 1]);
}
int mx = fail[n], m = 0;
while (mx) bd[++m] = n - mx, mx = fail[mx];
memset(f, 0x3f, sizeof(f)); f[0] = n;
bd[++m] = n, P = n;
for (int i = 1; i <= m; i++) {
if (i == m) work(bd[i], 0, 0);
else {
int j = i + 1;
while (j < m && bd[j + 1] - bd[j] == bd[i + 1] - bd[i]) j++;
work(bd[i], j - i , bd[i + 1] - bd[i]); i = j;
}
}
int ans = 0;
for (int i = 0; i < P; i++)
if (f[i] <= W) ans += (W - f[i]) / P + 1;
cout << ans << '\n';
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T--) solve();
return 0;
}
比较传统的字符串 + ds 题。
显然要建 AC 自动机。枚举长的串 \(i\),它的每一个后缀有贡献的至多只有一个,容易知道就是 fail 树对应的点祖先中非本身的最深点,这个是容易预处理的,所以不用枚举每一个 \(j\) 看是否合法,而只用检查这些可能的 \(j\) 是否是合法的,这样问题就变成了如何 check。
合法的必要条件还有当前后缀合法串的最左的左端点位置 \(>\) 其左端点。check 还有一个难点因为我们不会访问到一些不合法的点(比如 \(i\) 某个前缀的非最长后缀),因此知道了可能合法的串也无法直接检查是否在所有情况下都合法。但是注意到我们可以知道每个 \(j\) 在 \(i\) 一共出现了几次,只用检查合法的出现次数是否等于总的出现次数即可。
这个其实是一个比较诈骗的地方,但是有的时候一时半会儿想不到。一般来说,如果能访问所有合法的位置并且能算出所有情况,那么只需要检查合法情况数是否等于总的情况数,而不用算不合法的有多少个,最本质来说还是一种正难则反的思想。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5, M = 26;
string s[N];
int len[N], a[N][M], ts, id[N], fail[N], g[N], pid[N];
inline void insert(string s, int x) {
int now = 0; len[x] = s.size();
for (int i = 0; i < len[x]; i++) {
int c = s[i] - 'a';
if (!a[now][c]) a[now][c] = ++ts;
now = a[now][c];
}
id[now] = x, pid[x] = now;
}
vector <int> p[N];
int dfn[N], dts, R[N];
void dfs(int k) {
dfn[k] = ++dts;
for (auto i : p[k]) dfs(i);
R[k] = dts;
}
void build() {
queue <int> q;
for (int i = 0; i < M; i++)
if (a[0][i]) q.push(a[0][i]);
while (q.size()) {
int t = q.front(); q.pop();
g[t] = id[t] ? id[t] : g[fail[t]];
p[fail[t]].push_back(t);
for (int i = 0; i < M; i++) {
if (!a[t][i]) a[t][i] = a[fail[t]][i];
else fail[a[t][i]] = a[fail[t]][i], q.push(a[t][i]);
}
}
}
struct BIT {
int c[N];
vector <int> p;
inline void add(int x, int y) {
p.push_back(x);
for (; x < N; x += x & -x) c[x] += y;
}
inline int qry(int x) {
int res = 0;
for (; x; x -= x & -x) res += c[x];
return res;
}
inline void clear() {
for (auto x : p)
for (; x < N; x += x & -x) c[x] = 0;
p.clear();
}
} T;
int pos[N], cnt[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n; cin >> n;
for (int i = 1; i <= n; i++)
cin >> s[i], insert(s[i], i);
build(), dfs(0); int ans = 0;
for (int i = 1; i <= n; i++) {
int now = 0; T.clear();
for (int j = 0; j < len[i]; j++) {
now = a[now][s[i][j] - 'a'], pos[j + 1] = now;
T.add(dfn[now], 1);
}
unordered_set <int> o;
for (int j = len[i], mn = len[i] + 1; j; j--) {
int t = g[pos[j]]; if (t == i) t = g[fail[pos[j]]];
if (t && j - len[t] + 1 < mn)
o.insert(t), cnt[t]++, mn = j - len[t] + 1;
}
for (auto j : o) {
if (cnt[j] == T.qry(R[pid[j]]) - T.qry(dfn[pid[j]] - 1)) ans++;
cnt[j] = 0;
}
}
cout << ans << '\n';
return 0;
}
纯粹的字符串好题。
一共有三段,不妨将 \(X\) 和第一个 \(Y\) 放在一起记为 \(A\),这样的好处是 \(S\) 就变成两段 \(AZ\) 了可以直接枚举分界点。记 \(B=Y^{k-1}\),此时 \(T\) 形如 \(ABZ\)(\(m>n\) 所以 \(k>1\))。
但这样会丢失一些限制,比如 \(A\) 末尾必须包含 \(Y\),也即 \(|Y|\le LCS(A,B)\)。根据 border 理论,所有整周期都是最小整周期的倍数,那么我们就只用找出最小的符合条件的 \(|Y|=a\),然后问题形如给定若干对 \((a,b)\),算有多少 \(x\) 满足 \(a\mid x,x\mid m-n,x\le b\),扫描线一下是简单的。
问题就在于如何求这个 \(a\)。遗憾的是,对于所有长度相同的子串求最长 border 最低也要带个 \(\log\)(据题解所述,我也不知道咋做)。但是这题中有个限制 \(\le LCS(A,B)\),看似更复杂,实则简化了问题:因为当 \(|A|\) 增加 \(1\) 时,若 \(LCS(A, B)\) 归零则直接无解,否则 lcs 长度增加 \(1\),原先有的 border 一定有,还可能新加入一个长度为当前 lcs 的 border,只有这些情况,做完了。
实现可以用哈希来 check border 啥的。
#include <bits/stdc++.h>
#define int long long
using namespace std;
typedef unsigned long long ull;
const int N = 1e7 + 5;
const int base = 131;
char s[N], t[N];
ull h1[N], h2[N], pw[N];
int cnt[N];
vector <int> p[N];
inline ull get1(int l, int r) {
return h1[r] - h1[l - 1] * pw[r - l + 1];
}
inline ull get2(int l, int r) {
return h2[r] - h2[l - 1] * pw[r - l + 1];
}
inline bool chk(int l, int r, int x) {
int t = r - l + 1 - x;
return get2(l, l + t - 1) == get2(r - t + 1, r);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m >> s + 1 >> t + 1;
for (int i = 1; i <= n; i++)
h1[i] = h1[i - 1] * base + s[i] - 'a' + 1;
pw[0] = 1;
for (int i = 1; i <= m; i++) {
pw[i] = pw[i - 1] * base;
h2[i] = h2[i - 1] * base + t[i] - 'a' + 1;
}
for (int i = 1, len = 0, w = 0; i <= n; i++) {
if (get1(1, i) != get2(1, i) || get1(i + 1, n) != get2(i + m - n + 1, m)) continue;
while (len < i && t[i - len] == t[i + m - n - len]) {
len++;
if (!w && chk(i + 1, i + m - n, len)) w = len;
}
if (t[i - len + 1] != t[i + m - n - len + 1]) len = 0, w = 0;
if (w && (m - n) % w == 0) p[len].push_back(w);
}
int ans = 0;
for (int i = 1; i <= m; i++) {
if ((m - n) % i == 0) {
for (int j = 1; j * j <= i; j++)
if (i % j == 0) {
cnt[j]++;
if (j * j != i) cnt[i / j]++;
}
}
for (auto j : p[i]) ans += cnt[j];
}
cout << ans << '\n';
return 0;
}
*P9623 [ICPC 2020 Nanjing R] Baby's First Suffix Array Problem
本质考验 ds 功底。
答案就是是算有多少个 \(i\in [l,r]\) 且 \(S[i:r]<s[x:r]\)。
先求出原先的后缀数组,发现加上区间的限制后仍和原先的 \(rk\) 有很大的关系,变化的只可能是原来 \(x,i\) 存在后缀包含关系。具体地,对于 \(x\),\(i\) 有贡献当且仅当:
- \(rk_i<rk_x\) 且 \(x<i<r\)。
- \(rk_i<rk_x\) 且 \(l<i<x\) 且 \(LCP(i,x)\le r-x\)。
- \(rk_i>rk_x\) 且 \(x<i<r\) 且 \(LCP(i,x)>r-i\)。
第一个就是简单的二维数点,略。
第二个难处理的限制是 \(LCP(i,x)\),将其写成 \(\min_{j=i+1}^x h_j\),但还是与 \(i\) 和 \(x\) 都有关。注意到是一个区间的形式,于是对 \(sa\) 分治,只处理 \(i\le mid<x\) 的询问,从中点处拆开分离变量。
设 \(suf_j=\min_{k=j}^m h_k,pre_j=\min_{k=m+1}^j h+k\),那么限制就变成 \(\min(suf_{rk_i+1},pre_{rk_x})\le r-x\)。若 \(pre_{rk_x}\le r-x\) 则对 \(i\) 的 \(suf\) 没有限制,否则要求 \(suf_{rk_i+1}\le r-x\),也是一个二维数点的形式,可做。
第三种情况类似,不过稍微复杂一点,因为拆开变成 \(suf_{rk_x+1}> r-i\) 且 \(pre_{rk_i}> r-i\),但前者也就是对 \(i\) 多了一个 \(>r-suf_{rk_x+1}\) 的限制,也是一个二维数点。
总复杂度 \(O((n+m)\log^2n)\),不是那么好写。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
char s[N];
struct BIT {
int c[N]; vector <int> p;
inline void add(int x, int y) {
x++; p.push_back(x);
for (; x < N; x += x & -x) c[x] += y;
}
inline int qry(int x) {
x++; int res = 0;
for (; x; x -= x & -x) res += c[x];
return res;
}
inline int qry(int l, int r) {
return qry(r) - qry(l - 1);
}
void clear() {
for (auto x : p)
for (; x < N; x += x & -x) c[x] = 0;
p.clear();
}
} T;
int n, sa[N], rk[N], cnt[N], id[N], t[N << 1], h[N];
inline bool cmp(int x, int y, int len) {
return t[x] == t[y] && t[x + len] == t[y + len];
}
inline void build() {
int m = 200;
for (int i = 1; i <= m; i++) cnt[i] = 0;
for (int i = n + 1; i <= n + n; i++) rk[i] = 0;
for (int i = 1; i <= n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i; i--) sa[cnt[rk[i]]--] = i;
for (int len = 1; len < n; len <<= 1) {
int ts = 0;
for (int i = n; i > n - len; i--) id[++ts] = i;
for (int i = 1; i <= n; i++)
if (sa[i] > len) id[++ts] = sa[i] - len;
for (int i = 1; i <= m; i++) cnt[i] = 0;
for (int i = 1; i <= n; i++) cnt[rk[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i; i--) sa[cnt[rk[id[i]]]--] = id[i];
m = 0; memcpy(t, rk, sizeof(t));
for (int i = 1; i <= n; i++)
rk[sa[i]] = (m += !cmp(sa[i], sa[i - 1], len));
if (m == n) break;
}
for (int i = 1, H = 0; i <= n; i++) {
if (rk[i] == 1) { H = 0; continue; }
if (H) H--; int j = sa[rk[i] - 1];
while (s[i + H] == s[j + H]) H++; h[rk[i]] = H;
}
}
struct Query { int l, r, id; };
struct node { int l, r, ll, rr, id; };
vector <Query> Q[N];
int ans[N], pre[N], suf[N];
inline bool cmp1(node x, node y) {
return x.l < y.l;
}
inline bool cmp2(int x, int y) {
return sa[x] < sa[y];
}
void solve(int l, int r) {
if (l == r) return; int m = l + r >> 1;
solve(l, m), solve(m + 1, r);
suf[m + 1] = pre[m] = n + 1;
for (int i = m; i >= l; i--)
suf[i] = min(suf[i + 1], h[i]);
for (int i = m + 1; i <= r; i++)
pre[i] = min(pre[i - 1], h[i]);
vector <node> p1, p2;
for (int i = m + 1; i <= r; i++) {
int x = sa[i];
for (auto j : Q[x]) {
p1.push_back({x + 1, j.r, 0, n + 1, j.id});
if (pre[i] <= j.r - x)
p1.push_back({j.l, x - 1, 0, n + 1, j.id});
else p1.push_back({j.l, x - 1, 0, j.r - x, j.id});
}
}
for (int i = l; i <= m; i++) {
int x = sa[i];
for (auto j : Q[x]) {
int lim = max(x + 1, j.r - suf[i + 1] + 1);
if (lim <= j.r) p2.push_back({lim, j.r, j.r + 1, 2 * n + 1, j.id});
}
}
vector <node> p;
for (auto i : p1) {
p.push_back({i.r, 1, i.ll, i.rr, i.id});
p.push_back({i.l - 1, -1, i.ll, i.rr, i.id});
}
sort(p.begin(), p.end(), cmp1);
vector <int> a;
for (int i = l; i <= m; i++) a.push_back(i);
sort(a.begin(), a.end(), cmp2); T.clear();
for (int i = 0, j = 0; i < p.size(); i++) {
while (j < a.size() && sa[a[j]] <= p[i].l) T.add(suf[a[j] + 1], 1), j++;
ans[p[i].id] += T.qry(p[i].ll, p[i].rr) * p[i].r;
}
p.clear(), a.clear(), T.clear();
for (auto i : p2) {
p.push_back({i.r, 1, i.ll, i.rr, i.id});
p.push_back({i.l - 1, -1, i.ll, i.rr, i.id});
}
sort(p.begin(), p.end(), cmp1);
for (int i = m + 1; i <= r; i++) a.push_back(i);
sort(a.begin(), a.end(), cmp2);
for (int i = 0, j = 0; i < p.size(); i++) {
while (j < a.size() && sa[a[j]] <= p[i].l) T.add(pre[a[j]] + sa[a[j]], 1), j++;
ans[p[i].id] += T.qry(p[i].ll, p[i].rr) * p[i].r;
}
}
inline void solve() {
int m; cin >> n >> m >> s + 1; build();
for (int i = 1; i <= n; i++) Q[i].clear();
for (int i = 1; i <= m; i++) {
int l, r, x; cin >> l >> r >> x;
Q[x].push_back({l, r, i}), ans[i] = 1;
}
solve(1, n);
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T--) solve();
return 0;
}


浙公网安备 33010602011771号