2026 暑假梦熊青岛集训 B1

题单

2026.7.24 Day6 图论与网络流

课件

部分题目题解:

*P9140 [THUPC 2023 初赛] 背包

带权值的同余最短路板子。文章

类似正常的同余最短路,考虑选取一个基准物品。注意到正常的同余最短路中有要求不会两次经过同一个点,在需要最优化权值的题中,就是选取性价比 \(\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; 
}

*ARC176E Max Vector

经典模型,需要积累。

切糕模型,形如对若干个变量之间的取值大小关系有限制,最小割。

对于每个 \(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; 
}

*P10062 [SNOI2024] 拉丁方

虽然算是半个二分图边染色的板子,但还是很妙。

先考虑 \(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; 
}

*P9167 [省选联考 2023] 城市建造

很厉害的题目。

首先选的点肯定是一个联通块,进一步的,如果 \((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; 
}

P7516 [省选联考 2021 A/B 卷] 图函数

感觉非正解更牛的一集。

第一反应是 \(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

题面

P11699 [ROIR 2025] 酸雨

有点棒棒糖,但是硬控我 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; 
}

MXOJ 省选二十连测第十三套T2 特别的人

屏幕截图 2026-07-25 161032

\(\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

qoj3234 Fireflies

结论:记一个坐标的权值为各维坐标之和,那么答案为出现最多的权值的出现次数,同时这个权值应等于 \(\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{M-1-sum(S)}{n-1}=\sum_{k=0}^{n-1}\dbinom{M-1-sum(S_1)}{k}\dbinom{-sum(S_2)}{n-1-k} \]

需要利用广义二项式系数定义:\(\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。

部分题目题解:

*P4156 [WC2016] 论战捆竹竿

很好的题目,感觉第一次见的话难自己做出来。

相当于对于所有是 \(S\) 的周期的长度做一个背包,看能凑出多少个 \(\le W\) 的和。

但是 \(W\) 非常大 + 可行性背包,所以考虑同余最短路。

每一个可能的长度都跑一遍显然会超时,但是注意到 border 理论中周期的性质:可以被划分为 \(O(\log n)\) 个等差数列,考虑对于每个等差数列一起做。

对于一个 \((st,len,d)\) 的等差数列,以 \(st\) 作背包的基准值,那么转移就是

\[f_{(i+st+kd)\bmod st}\leftarrow f_i + kd + st,k<len \]

把正常按 \(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; 
}

*CF1483F Exam

比较传统的字符串 + 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;
}

*CF1909G Pumping Lemma

纯粹的字符串好题。

一共有三段,不妨将 \(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; 
}
posted @ 2026-07-24 19:43  happy_zero  阅读(14)  评论(1)    收藏  举报