2026 暑假梦熊青岛集训 B1 Part 1
不懂电脑还是博客园被干冒烟了,开两篇。
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;
}
夹带私货中,这题讲课 PPT 上没有,但是省集讲了,是道图论好题。
*P9531 [JOIST 2022] 复兴计划 / Reconstruction Project
不知道是不是 trick,但反正很厉害的一题。
稍微分析一下可以得到一条边在生成树中出现的次数一定是一段包含 \(w_i\) 的区间(这样可能不一定形成一棵树,但是这样当且仅当权值全为 \(0\) 的边形成环了,总权值还是对的)。
然后考虑对于询问分治。对于 \(X_l,X_m,X_r\) 分别求出在生成树上存在的边,若一条边在 \([l,r]\) 都出现了就不用递归下去了,加到答案里并将左右端点合并一下;若跨过 \(m\) 则两边都递归下去,否则根据 \(w\) 与 \(X_m\) 的大小关系判断要往哪边递归下去。
看上去是很暴力的过程,但考虑最终由于在完全包含的时候返回了,这就是一个类似线段树的拆分区间,一共只会在 \(O(\log)\) 个区间有贡献,总的复杂度就是 \(O(m\log^2q)\) 的。
#include <bits/stdc++.h>
#define int long long
#define pii pair <int, int>
#define fi first
#define se second
using namespace std;
const int N = 1e6 + 5;
struct node { int u, v, w; } ;
inline bool cmp(node x, node y) {
return x.w < y.w;
}
int ans1[N], ans2[N], X[N], tf[N], fa[N], siz[N];
inline int find(int x) {
return fa[x] == x ? x : find(fa[x]);
}
inline int findt(int x) {
return tf[x] == x ? x : tf[x] = findt(tf[x]);
}
stack <int> q;
inline void merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return;
if (siz[x] < siz[y]) swap(x, y);
q.push(y), fa[y] = x, siz[x] += siz[y];
}
pii b[N];
vector <bool> kruskal(vector <int> o, vector <node> p, int x) {
for (auto i : o) tf[i] = i;
int n = p.size(); vector <bool> res(n);
for (int i = 0; i < n; i++) b[i] = {abs(p[i].w - x), i};
sort(b, b + n);
for (int i = 0; i < n; i++) {
int u = p[b[i].se].u, v = p[b[i].se].v;
if (findt(u) ^ findt(v))
res[b[i].se] = 1, tf[findt(u)] = findt(v);
}
return res;
}
inline vector <node> chk(vector <node> p) {
vector <node> res;
for (auto i : p)
if (find(i.u) ^ find(i.v))
res.push_back({find(i.u), find(i.v), i.w});
return res;
}
void undo(int s) {
while (q.size() > s) {
int x = q.top(); q.pop();
siz[fa[x]] -= siz[x], fa[x] = x;
}
}
void solve(int l, int r, vector <int> o, vector <node> p) {
if (!p.size()) return;
vector <node> p1, p2;
int m = l + r >> 1, n = p.size(), now = q.size();
vector <bool> res = kruskal(o, p, X[m]);
vector <bool> A = kruskal(o, p, X[l]), B = kruskal(o, p, X[r]);
for (int i = 0; i < n; i++) {
if (!res[i]) {
if (p[i].w < X[m]) p1.push_back(p[i]);
else p2.push_back(p[i]);
} else if (!A[i] || !B[i])
p1.push_back(p[i]), p2.push_back(p[i]);
else {
int x = lower_bound(X + l, X + r + 1, p[i].w) - X;
ans1[l] += p[i].w, ans2[l]--, ans1[x] -= p[i].w, ans2[x]++;
ans1[x] -= p[i].w, ans2[x]++, ans1[r + 1] += p[i].w, ans2[r + 1]--;
merge(p[i].u, p[i].v);
}
}
for (auto &i : o) i = find(i);
sort(o.begin(), o.end());
o.erase(unique(o.begin(), o.end()), o.end());
if (l ^ r) solve(l, m, o, chk(p1)), solve(m + 1, r, o, chk(p2));
undo(now);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m; vector <node> p;
for (int i = 1, u, v, w; i <= m; i++)
cin >> u >> v >> w, p.push_back({u, v, w});
int q; cin >> q;
for (int i = 1; i <= q; i++) cin >> X[i];
sort(p.begin(), p.end(), cmp); vector <int> o;
for (int i = 1; i <= n; i++)
fa[i] = i, siz[i] = 1, o.push_back(i);
solve(1, q, o, p);
for (int i = 1; i <= q; i++) {
ans1[i] += ans1[i - 1], ans2[i] += ans2[i - 1];
cout << ans1[i] + ans2[i] * X[i] << '\n';
}
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;
}
2026.7.27 数据结构综合 1
除了 cf 那道都是不错的题,每道题都能有所收获。
部分题目题解:
小清新数据结构,不用复杂算法,考察功底。
这个东西在线根本做不了啊,分治似乎不好合并,于是考虑扫描线。
对 \(r\) 扫描线,那么应该要维护每个 \(l\) 的答案。假如固定了左端点 \(l'\),那么可以用线段树维护每个 \(l'\) 对应的答案,具体手玩一下就是区间覆盖。
但是 \(l'\) 不确定啊,并且 \(l'\) 的选择会影响到 \(r'\) 的选择。但是注意到合法的 \(l'\in[l,x]\) 至少满足 \(A[l\cdots r]=A[l'\cdots r]=A[l'\cdots r']\),所以的确只需要维护扫到 \(r\) 时每个 \(l'\) 对应的答案 \(f_{l'}\),然后线段树上查 \(\min_{i=l}^x f_i\) 即可。
但是还有一个问题是如何求 \(x\)。有一个很巧妙的方法,显然 \(l\) 对应的 \(x\) 不降。用并查集维护这个东西,然后再扫到 \(r\) 时将 \(pre_{a_r}\) 合并到 \(pre_{a_r+1}\) 上。总复杂度 \(O(n\log n)\)。
最关键的一步转化实际上是分离变量,利用强的限制将 \(l,r\) 的限制分成两步,这个本质上的要求是限制是“不可逆”的,假如某下不满足了继续做一定也不满足。
#include <bits/stdc++.h>
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
using namespace std;
const int N = 2e6 + 5;
const int INF = 1e9;
struct Segment {
int t[N << 2], tg[N << 2];
inline void pushup(int k) {
t[k] = min(t[ls(k)], t[rs(k)]);
}
inline void f(int k, int l, int r, int v) {
tg[k] = v, t[k] = v - r;
}
inline void pushdown(int k, int l, int r) {
if (tg[k] == -1) return; int m = l + r >> 1;
f(ls(k), l, m, tg[k]), f(rs(k), m + 1, r, tg[k]);
tg[k] = -1;
}
void build(int k, int l, int r) {
tg[k] = -1; int m = l + r >> 1;
if (l == r) return;
build(ls(k), l, m), build(rs(k), m + 1, r);
pushup(k);
}
void updata(int k, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) return f(k, l, r, v);
int m = l + r >> 1; pushdown(k, l, r);
if (L <= m) updata(ls(k), l, m, L, R, v);
if (R > m) updata(rs(k), m + 1, r, L, R, v);
pushup(k);
}
int query(int k, int l, int r, int L, int R) {
if (L <= l && r <= R) return t[k];
int m = l + r >> 1, res = INF; pushdown(k, l, r);
if (L <= m) res = query(ls(k), l, m, L, R);
if (R > m) res = min(res, query(rs(k), m + 1, r, L, R));
return res;
}
} T;
int pre[N], fa[N], a[N], ans[N];
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
struct node { int l, id; };
vector <node> p[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 >> a[i], fa[i] = i;
T.build(1, 1, n); int m; cin >> m;
for (int i = 1; i <= m; i++) {
int l, r; cin >> l >> r;
p[r].push_back({l, i});
}
for (int i = 1; i <= n; i++) {
int x = pre[a[i]]; T.updata(1, 1, n, x + 1, i, i);
if (x) fa[find(x)] = find(x + 1);
pre[a[i]] = i;
for (auto j : p[i])
ans[j.id] = T.query(1, 1, n, j.l, find(j.l)) + 1;
}
for (int i = 1; i <= m; i++) cout << ans[i] << '\n';
return 0;
}
感觉想到拆成四维偏序已经是极限了。
直接模拟太没有出路了,不妨直接考虑哪些操作能被触发。
初始序列有点烦,不妨也将其视作操作。初始序列看成操作还挺常见的。 易得 \(i\) 被触发当且仅当存在 \(j<i,[l_j,r_j]\cap[l_i,r_i]\ne \varnothing,y_j\ge x_i\),那么可以递推出一个操作能触发的 \(y\) 的 \(\max\)。
运用一点区间到单点的思想,或者取巧修改是单点是肯定不能依赖于全局一起做,需要拆贡献,其实是每个 \(a_i\) 转成单点的修改后是独立的,答案取个 \(\max\) 即可,因此是可以维护的。
但是上面区间交的是二维偏序,加上其它的限制是一个四维偏序,\(2\times 10^6 \log^3\) 咋过。
发现问题出在我们默认将区间交问题看成偏序问题,这并不是必须的。 实际上如果再次运用拆成单点的思想,枚举交的点,可以比较自然地引出将区间放到线段树上做一个类似标记永久化的东西,那么就可以直接在一只 \(\log\) 的时间内完成区间交的限制,剩下的套 cdq 分治即可。
还差一点的其实是 \(q\) 很大带两只 \(\log\) 不行,但是其实没必要一起做,把区间处理完单点的修改其实可以直接扫描线一只 \(\log\) 做,这样就做完了。
当遇到复杂度的瓶颈的时候,可以观察前面转化的过程中是否失掉了一些性质,比如可以利用数据结构的天然形状来刻画的结构是否有被转化掉了。
#include <bits/stdc++.h>
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
using namespace std;
const int N = 1e6 + 5;
const int INF = 2e9;
int n, a[N];
struct node {
int l, r, x, L, R, y, f, id;
} b[N];
struct Segment {
int t[N << 2], tg[N << 2];
queue <int> q;
Segment() {
memset(t, -0x3f, sizeof(t));
memset(tg, -0x3f, sizeof(tg));
}
inline void pushup(int k) {
t[k] = max(t[ls(k)], t[rs(k)]), q.push(k);
}
inline void f(int k, int v) {
t[k] = max(t[k], v), tg[k] = max(tg[k], v);
q.push(k);
}
inline void pushdown(int k) {
if (tg[k] > -INF)
f(ls(k), tg[k]), f(rs(k), tg[k]), tg[k] = -INF;
}
void updata(int k, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) return f(k, v);
int m = l + r >> 1; pushdown(k);
if (L <= m) updata(ls(k), l, m, L, R, v);
if (R > m) updata(rs(k), m + 1, r, L, R, v);
pushup(k);
}
int query(int k, int l, int r, int L, int R) {
if (L <= l && r <= R) return t[k];
int m = l + r >> 1, res = -INF; pushdown(k);
if (L <= m) res = query(ls(k), l, m, L, R);
if (R > m) res = max(res, query(rs(k), m + 1, r, L, R));
return res;
}
void clear() {
while (!q.empty()) {
int x = q.front(); q.pop();
t[x] = tg[x] = -INF;
}
}
} T;
inline bool cmpx(node x,node y) {
return x.x < y.x;
}
inline bool cmpy(node x, node y) {
return x.y < y.y;
}
inline bool cmpn(node x, node y) {
return x.id < y.id;
}
void cdq(int l, int r) {
if (l == r) return;
int m = l + r >> 1; cdq(m + 1, r);
sort(b + l, b + m + 1, cmpy);
sort(b + m + 1, b + r + 1, cmpx);
for (int i = l, j = m + 1; i <= m; i++) {
while (j <= r && b[j].x <= b[i].y)
T.updata(1, 1, n, b[j].l, b[j].r, b[j].f), j++;
b[i].f = max(b[i].f, T.query(1, 1, n, b[i].L, b[i].R));
}
sort(b + l, b + m + 1, cmpn);
T.clear(), cdq(l, m);
}
struct Query {
int x, v, id;
} Q[N];
int ans[N], lst[N], op[N];
inline bool cmp(Query x, Query y) {
return x.v < y.v;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int m, k; cin >> n >> m >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= m; i++) {
cin >> b[i].l >> b[i].r >> b[i].x >> b[i].L >> b[i].R >> b[i].y;
b[i].f = b[i].y, b[i].id = i;
}
cdq(1, m); int ts = 0;
for (int i = 1; i <= n; i++)
Q[++ts] = {i, a[i], i};
for (int i = 1, x, y; i <= k; i++)
cin >> x >> y, Q[++ts] = {x, y, i + n}, op[i] = x;
sort(Q + 1, Q + 1 + ts, cmp);
sort(b + 1, b + 1 + m, cmpx);
for (int i = 1, j = 1; i <= ts; i++) {
while (j <= m && b[j].x <= Q[i].v)
T.updata(1, 1, n, b[j].l, b[j].r, b[j].f), j++;
ans[Q[i].id] = max(Q[i].v, T.query(1, 1, n, Q[i].x, Q[i].x));
}
multiset <int> o;
for (int i = 1; i <= n; i++)
o.insert(ans[i]), lst[i] = i;
for (int i = 1; i <= k; i++) {
int x = op[i];
o.erase(o.find(ans[lst[x]])), o.insert(ans[i + n]);
cout << (*o.rbegin()) << '\n'; lst[x] = i + n;
}
return 0;
}
可能已经变成经典题了。
楼房重建是 \(O(q\log^2n)\) 的,没有前途。然后发现类似的基于序列上的结构无法更优。
考虑换维!对序列维扫描线,维护时间维。 每次相当于把一段时间对 \(x\) chkmin,然后答案是成功 chkmin 的次数,这个可以直接 segment beats 维护,然后就做完了,复杂度 \(O(q\log n)\) 。
#include <bits/stdc++.h>
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
using namespace std;
const int N = 1e6 + 5;
const int INF = 2e9;
struct Segment {
int t[N << 2], mx[N << 2], se[N << 2];
inline void pushup(int k) {
mx[k] = max(mx[ls(k)], mx[rs(k)]);
se[k] = max(mx[k] == mx[ls(k)] ? se[ls(k)] : mx[ls(k)],
mx[k] == mx[rs(k)] ? se[rs(k)] : mx[rs(k)]);
}
inline void f(int k, int x, int v) {
if (mx[k] > x) mx[k] = x, t[k] += v;
}
inline void pushdown(int k) {
if (!t[k]) return;
f(ls(k), mx[k], t[k]), f(rs(k), mx[k], t[k]);
t[k] = 0;
}
void build(int k, int l, int r) {
mx[k] = INF;
if (l == r) return; int m = l + r >> 1;
build(ls(k), l, m), build(rs(k), m + 1, r);
pushup(k);
}
void updata(int k, int l, int r, int L, int R, int x) {
if (x >= mx[k]) return;
if (L <= l && r <= R && se[k] < x) return f(k, x, 1);
int m = l + r >> 1; pushdown(k);
if (L <= m) updata(ls(k), l, m, L, R, x);
if (R > m) updata(rs(k), m + 1, r, L, R, x);
pushup(k);
}
int query(int k, int l, int r, int x) {
if (l == r) return t[k];
int m = l + r >> 1; pushdown(k);
if (x <= m) return query(ls(k), l, m, x);
else return query(rs(k), m + 1, r, x);
}
} T;
struct node { int op, x, v; };
vector <node> p[N];
int ans[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m; T.build(1, 0, m);
for (int i = 1, x; i <= n; i++)
cin >> x, p[i].push_back({0, 0, x});
for (int i = 1; i <= m; i++) {
int op, x, v; cin >> op >> x;
if (op == 1) cin >> v, p[x].push_back({0, i, v});
else p[x].push_back({1, i, 0});
}
for (int i = n; i; i--) {
int pre = m + 1;
reverse(p[i].begin(), p[i].end());
for (auto j : p[i])
if (!j.op) T.updata(1, 0, m, j.x, pre - 1, j.v), pre = j.x;
for (auto j : p[i])
if (j.op) ans[j.x] = T.query(1, 0, m, j.x);
}
for (int i = 1; i <= m; i++)
if (ans[i]) cout << ans[i] <<'\n';
return 0;
}
分治练习题。
这个背包是没有任何限制的,相当于一个黑盒,支持 \(O(m)\) 插入,\(O(m^2)\) 合并。合并的复杂度太大了不能用。但是注意到很像两个缺一分治的形式,而最后询问的只是模 \(m\) 为 \(0\) 的单点值,这个是可以合并的,所以总的来说支持对于每个询问得到两个背包,恰好对应两个“缺一”。
区间是很难做的,因为还要管左端点,考虑拆成四个由前缀+后缀限制组成的,然后分治,对于一个 \(i\in[1,x],j\in[y,n]\) 的限制,在 \(x\le m<y\) 时处理。
当前分治区间外部的一段前缀和后缀可以在分治过程中维护,内部需要对 \([l,r]\) 进行缺一分治,直接继承外部的信息,然后求出关于缺一位置做前/后缀和的背包,然后再合并即可。
询问合并一次背包,然后分治套缺一分治,所以总复杂度 \(O(nm\log^2n)+qm\)。时限稍微有点紧,但背包封装 + 略微卡常也 4.5s 跑过去了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e4 + 5, M = 205, K = 1e6 + 5;
const int P = 1e9 + 7;
inline int mod(int x) { return x >= P ? x - P : x; }
inline void add(int &x, int y) { x = mod(x + y); }
int m, a[N], b[N];
inline int Mod(int x) { return x >= m ? x - m : x; }
struct poly {
int f[M];
poly(bool op = 1) { memset(f, 0, sizeof(f)); f[0] = op; }
inline friend poly operator +(poly &x, poly &y) {
poly z = poly();
for (int i = 0; i < m; i++) z.f[i] = mod(x.f[i] + y.f[i]);
return z;
}
inline friend poly operator +(poly &x, int y) {
poly z = x;
for (int i = 0; i < m; i++) add(z.f[Mod(i + a[y])], x.f[i]);
for (int i = 0; i < m; i++) add(z.f[Mod(i + b[y])], x.f[i]);
return z;
}
inline friend int operator *(poly &x, poly &y) {
int ans = 0;
for (int i = 0; i < m; i++)
ans = (ans + x.f[i] * y.f[Mod(m - i)]) % P;
return ans;
}
};
struct Poly {
poly f, g;
Poly(poly a = poly(), poly b = poly(0)) { f = a, g = b; }
inline friend Poly operator +(Poly &x, Poly &y) {
return Poly(x.f + y.f, x.g + y.g);
}
inline friend Poly operator +(Poly &x, int y) {
return Poly(x.f + y, x.g + y);
}
inline friend Poly operator *(Poly &x, int &y) {
poly t = x.g + y; return Poly(x.f + y, x.f + t);
}
} pre[N], suf[N];
int ans[K];
struct node { int l, r, op, id; };
void solve_pre(int l, int r, Poly &x) {
if (l == r) return pre[l] = x * l, void();
int m = l + r >> 1; Poly y = x;
for (int i = l; i <= m; i++) x = x * i;
for (int i = m + 1; i <= r; i++) y = y + i;
solve_pre(l, m, y), solve_pre(m + 1, r, x);
}
void solve_suf(int l, int r, Poly &x) {
if (l == r) return suf[r] = x * r, void();
int m = l + r >> 1; Poly y = x;
for (int i = l; i <= m; i++) x = x + i;
for (int i = m + 1; i <= r; i++) y = y * i;
solve_suf(l, m, y), solve_suf(m + 1, r, x);
}
void solve(int l, int r, vector <node> &p, Poly L, Poly R) {
if (l == r) return ; int m = l + r >> 1;
Poly tL = L, tR = R;
for (int i = l; i <= m; i++) tL = tL * i;
for (int i = m + 1; i <= r; i++) tR = tR * i;
vector <node> p1, p2, o;
for (auto i : p) {
if (i.r <= m) p1.push_back(i);
else if (i.l > m) p2.push_back(i);
else o.push_back(i);
}
p.clear(); p.shrink_to_fit();
solve(l, m, p1, L, tR), solve(m + 1, r, p2, tL, R);
solve_pre(l, m, L), solve_suf(m + 1, r, R);
for (auto i : o)
ans[i.id] = (ans[i.id] + i.op * (pre[i.l].g * suf[i.r].g)) % P;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, q; cin >> n >> m >> q; vector <node> p;
for (int i = 1; i <= n; i++) cin >> a[i] >> b[i];
for (int i = 1; i <= q; i++) {
int l1, r1, l2, r2; cin >> l1 >> r1 >> l2 >> r2;
p.push_back({r1, l2, 1, i});
if (l1 > 1) p.push_back({l1 - 1, l2, P - 1, i});
if (r2 < n) p.push_back({r1, r2 + 1, P - 1, i});
if (l1 > 1 && r2 < n) p.push_back({l1 - 1, r2 + 1, 1, i});
}
solve(1, n, p, Poly(), Poly());
for (int i = 1; i <= q; i++) cout << ans[i] << '\n';
return 0;
}
简单题,这有 *2800?不要做,不是好题。
分治,然后枚举跨越 \(mid\) 的区间左端点 \(l'\),分讨 \(\min,\max\) 在哪一边取到一共有三种情况,分界点随 \(l'\) 减小而单调不降,双指针再随便开个桶维护一下即可。
#include <bits/stdc++.h>
#define int long long
#define pc(i) __builtin_popcountll(i)
using namespace std;
const int N = 1e6 + 5, M = 70;
int a[N], ans, pren[N], prex[N], cnt1[M], cnt2[M];
void solve(int l, int r) {
if (l == r) return; int m = l + r >> 1;
solve(l, m), solve(m + 1, r);
pren[m + 1] = prex[m + 1] = a[m + 1]; int c = 0;
for (int i = m + 2; i <= r; i++) {
pren[i] = min(pren[i - 1], a[i]);
prex[i] = max(prex[i - 1], a[i]);
}
for (int i = m + 1; i <= r; i++) c += pc(pren[i]) == pc(prex[i]);
int mn = a[m], mx = a[m], j = m, k = m;
for (int i = m; i >= l; i--) {
mn = min(mn, a[i]), mx = max(mx, a[i]);
while (j < r && mn <=pren[j + 1] && prex[j + 1] <= mx)
j++, cnt1[pc(pren[j])]--, cnt2[pc(prex[j])]--;
while (k < j || k < r && !(pren[k + 1] <= mn && mx <= prex[k + 1])){
k++, cnt1[pc(pren[k])]++, cnt2[pc(prex[k])]++;
c -= pc(pren[k]) == pc(prex[k]);
}
ans += (pc(mn) == pc(mx)) * (j - m) + c;
if (j < k && pren[j + 1] < mn) ans += cnt1[pc(mx)];
else ans += cnt2[pc(mn)];
}
memset(cnt1, 0, sizeof(cnt1)), memset(cnt2, 0, sizeof(cnt2));
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n; cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
solve(1, n), cout << ans + n << '\n';
return 0;
}
出栈序应用题。
如果是序列上的问题,写出 dp 式子,容易用线段树套李超树完成。
转到树上,一个想法是树剖,然后形如 \(O(n\log n)\) 次查询重链前缀和 \(O(n)\) 次查询重链区间,前者可以类似可持久化李超树一样做,后者上线段树套李超树可以做到 \(O(n\log^2n)\)。
但是有一个更妙的做法。直接查询当前点到可以到达的最浅点的出栈序的区间,那些在分叉上的点还没有被更新所以不会影响,这样就只需要 \(O(n)\) 次访问线段树套李超树,也是 \(2\log\) 的。
出栈序的适用范围:更新是在线的;只从父亲转移而来。 感觉用这个少一个 \(\log\) 应该有一定用处的,但不知道为什么目前只看过这一道题。
#include <bits/stdc++.h>
#define int long long
#define ls(k) t[k].ls
#define rs(k) t[k].rs
using namespace std;
const int N = 2e5 + 5, V = 1e6;
const int INF = 2e18;
vector <int> p[N];
int d[N], a[N], b[N], L[N], dfn[N], ts, q[N], top, g[N];
void dfs0(int k) {
q[++top] = k; int l = 1, r = top, res = 0;
while (l <= r) {
int mid = l + r >> 1;
if (d[k] - d[q[mid]] <= L[k])
res = mid, r = mid - 1;
else l = mid + 1;
}
g[k] = q[res];
for (auto i : p[k]) dfs0(i);
dfn[k] = ++ts, top--;
}
struct line {
int k, b;
line(int K = 0, int B = INF) { k = K, b = B; }
inline int cal(int x) { return k * x + b; }
};
inline bool cmp(line a, line b, int x) {
return a.cal(x) < b.cal(x);
}
inline line chmax(line a, line b, int x) {
return cmp(a, b, x) ? a : b;
}
struct Segment {
struct node {
int ls, rs; line v;
} t[N << 6];
int ts, rt[N << 2];
inline void f(int &k, int l, int r, line x) {
if (l > r) return; if (!k) k = ++ts;
int m = l + r >> 1;
if (cmp(x, t[k].v, m)) swap(t[k].v, x);
if (cmp(x, t[k].v, l)) f(ls(k), l, m, x);
if (cmp(x, t[k].v, r)) f(rs(k), m + 1, r, x);
}
int query(int k, int l, int r, int x) {
if (!k) return INF; int m = l + r >> 1;
if (l == r) return t[k].v.cal(x);
if (x <= m) return min(t[k].v.cal(x), query(ls(k), l, m, x));
else return min(t[k].v.cal(x), query(rs(k), m + 1, r, x));
}
} T;
#undef ls
#undef rs
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
void modify(int k, int l, int r, int x, line v) {
T.f(T.rt[k], 0, V, v);
if (l == r) return ; int m = l + r >> 1;
if (x <= m) modify(ls(k), l, m, x, v);
else modify(rs(k), m + 1, r, x, v);
}
int query(int k, int l, int r, int L, int R, int x) {
if (L <= l && r <= R) return T.query(T.rt[k], 0, V, x);
int m = l + r >> 1, res = INF;
if (L <= m) res = min(res, query(ls(k), l, m, L, R, x));
if (R > m) res = min(res, query(rs(k), m + 1, r, L, R, x));
return res;
}
int n, ans[N];
void dfs(int k) {
if (k > 1) {
ans[k] = query(1, 1, n, dfn[k] + 1, dfn[g[k]], a[k]) ;
ans[k] += d[k] * a[k] + b[k];
}
modify(1, 1, n, dfn[k], line(-d[k], ans[k]));
for (auto i : p[k]) dfs(i);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int _; cin >> n >> _;
for (int i = 2; i <= n; i++) {
int f, x; cin >> f >> x >> a[i] >> b[i] >> L[i];
p[f].push_back(i), d[i] = d[f] + x;
}
dfs0(1), dfs(1);
for (int i = 2; i <= n; i++) cout << ans[i] << '\n';
return 0;
}
也是一个好题。
总体的思路是,对于每个操作,求出它最后被弹出的时间,然后对于所有相同权值的操作去重一下。最关键的就是如何求出每个操作所有位置最晚被全部弹出的时间。
似乎没有一个特别好的数据结构来刻画,但是注意到可以通过较小的代价一起处理一些询问。具体来说,如果要处理 \(pos=[l,r]\) 的询问,共有 \(m\) 个,那么可以对这 \(m\) 个询问双指针+线段树维护最后被全部删去的时间,也就是说可以话 \(m\log m\) 的得到 \(pos=[l,r]\) 的询问(注意只能得到恰好为这个区间的,包含于其的只是会影响需要被加入在这个过程中无法被算出,因为实际上不符合双指针单调性)。
那么两种思路,一种分块,一种线段树分治。 前者就是直接类似这样做的,但需要做到严格根号还要平衡复杂度,而后者拆分成的区间只有 \(\log\) 个,考虑后者。但是后者的一个问题是不能把祖先会影响到的全部加进来(但是儿子可以,复杂度同拆分区间),但是这些全部都是完全包含 \([l,r]\) 的其实是等价的,用一个树状数组维护下每个时间有多少个被加入就可以了。
关键不是这道题怎么做,而是体会一下这种拆分区间一起做的思路,和 P7220 异曲同工。
#include <bits/stdc++.h>
#define pii pair <int, int>
#define fi first
#define se second
#define ls(k) (k << 1)
#define rs(k) (k << 1 | 1)
using namespace std;
const int N = 1e5 + 5;
struct node { int l, r, x, t; };
int n, m, a[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 (; x; x -= x & -x) res += c[x];
return res;
}
inline int qry(int l, int r) {
return qry(r) - qry(l - 1);
}
inline int kth(int x) {
int res = 0;
for (int i = 20; ~i; i--) {
int t = res + (1 << i);
if (t <= m && x >= c[t]) res = t, x -= c[t];
}
return res;
}
} B;
struct Segment {
int t[N << 2], tg[N << 2];
inline void pushup(int k) {
t[k] = min(t[ls(k)], t[rs(k)]);
}
inline void f(int k, int v) { t[k] += v, tg[k] += v; }
inline void pushdown(int k) {
f(ls(k), tg[k]), f(rs(k), tg[k]), tg[k] = 0;
}
void build(int k, int l, int r) {
t[k] = -a[l]; int m = l + r >> 1;
if (l == r) return;
build(ls(k), l, m), build(rs(k), m + 1, r);
pushup(k);
}
void updata(int k, int l, int r, int L, int R, int v) {
if (L <= l && r <= R) return f(k, v);
int m = l + r >> 1; pushdown(k);
if (L <= m) updata(ls(k), l, m, L, R, v);
if (R > m) updata(rs(k), m + 1, r, L, R, v);
pushup(k);
}
} T;
int mn[N];
void solve(int k, int l, int r, vector <node> p) {
if (l > r || !p.size()) return; int m = l + r >> 1;
vector <node> p1, p2; int len = p.size();
for (int i = 0, j = -1; i < len; i++) {
while (j < i) j++, T.updata(1, 1, n, p[j].l, p[j].r, 1);
if (p[i].l ^ l || p[i].r ^ r) {
if (p[i].r <= m) p1.push_back(p[i]);
else if (p[i].l > m) p2.push_back(p[i]);
else {
node a = p[i], b = p[i]; a.r = m, b.l = m + 1;
p1.push_back(a), p2.push_back(b);
}
T.updata(1, 1, n, p[i].l, p[i].r, -1);
continue;
}
while (j < len - 1 && B.qry(p[i].t, p[j + 1].t) + T.t[k] <= 0)
j++, T.updata(1, 1, n, p[j].l, p[j].r, 1);
int v = B.kth(B.qry(p[i].t) - T.t[k]);
if (~j && v < p[j].t) v = p[j].t - 1;
mn[p[i].t] = max(mn[p[i].t], v);
T.updata(1, 1, n, p[i].l, p[i].r, -1);
}
for (auto i : p)
if (i.l == l && i.r == r) B.add(i.t, 1);
if (l ^ r) solve(ls(k), l, m, p1), solve(rs(k), m + 1, r, p2);
for (auto i : p)
if (i.l == l && i.r == r) B.add(i.t, -1);
}
vector <pii> p[N];
int X[N], ans[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m; int mx = 0;
for (int i = 1; i <= n; i++) cin >> a[i];
vector <node> o; T.build(1, 1, n);
for (int i = 1; i <= m; i++) {
int l, r, x, t; cin >> l >> r >> x;
o.push_back({l, r, x, i});
X[i] = x, mx = max(mx, x);
}
solve(1, 1, n, o);
for (int i = 1; i <= m; i++)
p[X[i]].push_back({i, mn[i]});
for (int i = 1; i <= mx; i++) {
int pre = 0;
for (auto j : p[i]) {
if (pre < j.se)
ans[max(pre + 1, j.fi)]++, ans[j.se + 1]--;
pre = max(pre, j.se);
}
}
for (int i = 1; i <= m; i++)
ans[i] += ans[i - 1], cout << ans[i] << '\n';
return 0;
}
*P9530 [JOIST 2022] 鱼 2 / Fish 2
JOI 怎么这么多 fish,都是好题。
看似描述很复杂,其实很简单:当前目标鱼不断吃掉能吃的直到吃完或吃不动了。
考虑全局一个极大的“吃”的区间 \([l,r]\) 满足:\(\sum_{i=l}^r a_i<\min(a_{l-1},a_{r+1})\)(设 \(a_0=a_{n+1}=+\infty\)),称这样的区间为坏区间。那么根据经典观察,坏区间区间要么包含,要么不交,并且对于一条鱼,其所在的坏区间最多只有 \(\log V\) 个。
进一步的观察,如果一个坏区间没有到达当前左端点右端点的任意一个,那么无论如何都不可能贡献到答案,否则可能在扩展的过程中吃到一些区间外的东西而能继续扩展,于是对于任意一个 \([l,r]\),其有用的坏区间实际上只有 \(O(\log V)\) 个,带单点修改,考虑用线段树维护这个东西。
每个区间有用的坏区间有三种:到左端点但每到右端点;到右端点但没到左端点;同时到左右端点,分别称为一、二、三类坏区间。合并左右儿子时,可以直接继承左儿子的一类、右儿子的二类坏区间,然后尝试将剩下的拓展,
以拓展左儿子的一、三类区间为例(这俩显然可以一起做)。按左端点从右到左扫描,维护当前能拓展到的右端点(显然,右端点只可能在右儿子的有用坏区间的右端点中产生),这个随着左端点向左是单调不降,如果左右都扩展不动了就判断是否“有用”然后加入答案(合并当前到上一次拓展不了中间经过的所有左端点)。具体地,需要维护每个有用区间被阻碍的那一边以及对应的小鱼的数量。
复杂度 \(O((n+q\log n)\log V)\),膜拜 alex-wei 老师的写法,极其优美。
#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 = 1e5 + 5;
struct node { int sum, cnt, lim; };
struct Tree {
int lw, rw, sum, cnt;
vector <node> pl, pr;
friend Tree operator +(Tree a, Tree b) {
Tree c = {a.lw, b.rw, a.sum + b.sum, 0, a.pl, b.pr};
vector <node> tl = a.pr, tr = b.pl, ansl, ansr;
tl.push_back({a.sum, a.cnt, 0}), tr.push_back({b.sum, b.cnt, 0});
int j = -1, cnt = tl[0].cnt, sum = 0;
for (int i = 0; ;) {
sum = tl[i].sum + (~j ? tr[j].sum : 0);
if (sum >= (~j ? tr[j].lim : b.lw) && j + 1 < tr.size()) j++;
else if (i == tl.size() - 1) break;
else {
if (sum < tl[i].lim) {
if (j + 1 == tr.size()) ansl.push_back({sum, cnt, tl[i].lim});
cnt = 0;
}
cnt += tl[++i].cnt;
}
}
if (j + 1 == tr.size()) c.cnt += cnt;
else c.pl.push_back({sum, cnt, ~j ? tr[j].lim : b.lw});
swap(tl, tr), swap(ansl, ansr);
j = -1, cnt = tl[0].cnt, sum = 0;
for (int i = 0; ;) {
sum = tl[i].sum + (~j ? tr[j].sum : 0);
if (sum >= (~j ? tr[j].lim : a.rw) && j + 1 < tr.size()) j++;
else if (i == tl.size() - 1) break;
else {
if (sum < tl[i].lim) {
if (j + 1 == tr.size()) ansl.push_back({sum, cnt, tl[i].lim});
cnt = 0;
}
cnt += tl[++i].cnt;
}
}
if (j + 1 == tr.size()) c.cnt += cnt;
else c.pr.push_back({sum, cnt, ~j ? tr[j].lim : a.rw});
for (auto i : ansl) c.pl.push_back(i);
for (auto i : ansr) c.pr.push_back(i);
return c;
}
} t[N << 2];
void pushup(int k) {
t[k] = t[ls(k)] + t[rs(k)];
}
void build(int k, int l, int r) {
if (l == r) {
int x; cin >> x;
return t[k] = {x, x, x, 1}, void();
}
int m = l + r >> 1;
build(ls(k), l, m), build(rs(k), m + 1, r);
pushup(k);
}
void modify(int k, int l, int r, int x, int v) {
if (l == r) return t[k] = {v, v, v, 1}, void();
int m = l + r >> 1;
if (x <= m) modify(ls(k), l, m, x, v);
else modify(rs(k), m + 1, r, x, v);
pushup(k);
}
Tree query(int k, int l, int r, int L, int R) {
if (L <= l && r <= R) return t[k];
int m = l + r >> 1;
if (L > m) return query(rs(k), m + 1, r, L, R);
if (R <= m) return query(ls(k), l, m, L, R);
return query(ls(k), l, m, L, R) + query(rs(k), m + 1, r, L, R);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n; cin >> n; build(1, 1, n);
int q; cin >> q;
while (q--) {
int op, x, y; cin >> op >> x >> y;
if (op == 1) modify(1, 1, n, x, y);
else cout << query(1, 1, n, x, y).cnt << '\n';
}
return 0;
}
2026.7.29 day9 数据结构综合 2
题目质量明显比前一天低,但下面三道都挺好的(ynoi 看看就得了)。授课 + 课件不如直接看题解,本质是给了一份题单。
唉,YNOI。让人有一种想砸电脑的冲动,但不能否认它题目的质量。
设 \(c(i,l,r)\) 表示 \(i\) 子树内在 \([l,r]\) 内点的个数,\(ans(i,l,r)=\dbinom{c(i,l,r)}{2}\),则答案就是 \(ans(x,l,r)-\sum_{i\in son(x)} ans(i,l,r)\)。
直接求最多 \(O(n^2)\) 次二维数点,无法接受。它都 YNOI 了,那肯定要想一想根号算法。
比较容易想到根号分治。对于儿子数 \(\le \sqrt n\) 的点直接暴力求,同时注意到加点次数为 \(n\),所以可以使用分块将复杂度平衡到 \(O(n\sqrt n)\)。
儿子数 \(>\sqrt n\) 的点 \(x\) 只有 \(O(\sqrt n)\) 个,考虑把子树内所有点找出来对 \(x\) 一起做,那么就变成小 Z 的袜子。但是注意到加点的话一共得加 \(O(n \sqrt n)\) 再乘上莫队的复杂度直接坠机,所以需要尝试保障所有加的点的数量和。
注意到我们可以花 \(1\) 的代价处理掉一个子树,贪心地,肯定是要处理掉大的。事实上,花费 \(\sqrt{n}\) 的代价处理掉前 \(\sqrt{n}\) 大的子树,那么剩下的每个子树大小和都不超过父亲大小 \(\frac{1}{\sqrt n}\),那么每个点都只会被加 \(O(1)\) 次,所以莫队总点数就是 \(O(n)\) 的,可以接受。
还有一个问题是空间也带 \(\sqrt n\),按子树大小从大到小 dfs 后需要询问的子树的 dfn 序就是连续的,下一次要减的就是上一次的答案,可以等当前这个做完了再加下一个点到 vector 中。
总复杂度 \(O(n\sqrt n)\),实测阈值取小了会优很多。它都 ynoi 了,不用卡常就知足吧。
#include <bits/stdc++.h>
#define pii pair <int, int>
#define fi first
#define se second
using namespace std;
const int N = 2e5 + 5;
const int B = 400, T = 100;
typedef long long ll;
inline ll cal(ll x) { return x * (x - 1) / 2; }
int n, mxB, id[N], L[N], R[N];
struct {
int s1[N], s2[N];
inline void upd(int x, int v) {
int y = id[x];
for (int i = y; i <= mxB; i++) s1[i]++;
for (int i = x; i <= R[y]; i++) s2[i]++;
}
inline int qry(int x) {
return x ? s1[id[x] - 1] + s2[x] : 0;
}
inline int qry(int l, int r) {
return qry(r) - qry(l - 1);
}
} S;
vector <int> p[N], pa[N], pb[N];
int siz[N], dfn[N], r[N], b[N], ts;
inline bool cmp(int x, int y) {
return siz[x] > siz[y];
}
void dfs(int k, int fa) {
siz[k] = 1;
for (auto i : p[k])
if (i ^ fa) {
dfs(i, k), siz[k] += siz[i];
pa[k].push_back(i);
}
sort(pa[k].begin(), pa[k].end(), cmp);
while (pa[k].size() > T)
pb[k].push_back(pa[k].back()), pa[k].pop_back();
}
void dfs1(int k) {
b[dfn[k] = ++ts] = k;
for (auto i : pa[k]) dfs1(i);
for (auto i : pb[k]) dfs1(i);
r[k] = ts;
}
ll ans[N], pre1[N], pre2[N], lst[N], cnt[N], res;
bool fl[N];
pii a[N];
struct node { int x, l, r, op, id; } ;
vector <node> f[N], of[N], og[N];
inline bool cmpt(node x, node y) {
if (id[x.l] ^ id[y.l]) return id[x.l] < id[y.l];
else return x.r < y.r;
}
inline void add(int x) { res += cnt[x]++; }
inline void del(int x) { res -= --cnt[x]; }
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int m, rt; cin >> n >> m >> rt;
for (int i = 1; i <= n; i++) {
id[i] = id[i - 1] + (i % B == 1);
if (!L[id[i]]) L[id[i]] = i; R[id[i]] = i;
}
mxB = id[n];
for (int i = 1; i < n; i++) {
int u, v; cin >> u >> v;
p[u].push_back(v), p[v].push_back(u);
}
dfs(rt, 0), dfs1(rt);
for (int i = 1; i <= n; i++)
for (auto j : pa[i]) lst[j] = pa[i].back();
for (int i = 1; i <= m; i++) {
int l, rr, x; cin >> l >> rr >> x;
of[r[x]].push_back({x, l, rr, 1, i});
of[dfn[x] - 1].push_back({x, l, rr, -1, i});
if (pa[x].size()) {
int t = pa[x][0];
og[r[t]].push_back({t, l, rr, 1, i});
og[dfn[t] - 1].push_back({t, l, rr, -1, i});
}
if (pb[x].size()) f[x].push_back({0, l, rr, 1, i});
}
for (int i = 1; i <= n; i++) {
S.upd(b[i], 1);
for (auto j : of[i]) {
int w = S.qry(j.l, j.r);
if (j.op == -1) pre1[j.id] = w;
else ans[j.id] += cal(w - pre1[j.id]);
}
for (auto &j : og[i]) {
int w = S.qry(j.l, j.r);
if (j.op == -1) { pre2[j.id] = w; continue; }
ans[j.id] -= cal(w - pre2[j.id]), pre2[j.id] = w;
if (j.x != lst[j.x])
j.x = b[r[j.x] + 1], og[r[j.x]].push_back(j);
}
og[i].clear(), og[i].shrink_to_fit();
}
for (int x = 1; x <= n; x++) {
if (!f[x].size()) continue; int ts = 0;
for (auto i : pb[x])
for (int j = dfn[i]; j <= r[i]; j++)
a[++ts] = {b[j], i};
sort(a + 1, a + 1 + ts);
for (auto &i : f[x]) {
i.l = lower_bound(a + 1, a + 1 + ts, (pii){i.l, 0}) - a;
i.r = lower_bound(a + 1, a + 1 + ts, (pii){i.r + 1, 0}) - a - 1;
}
sort(f[x].begin(), f[x].end(), cmpt);
int l = 1, r = 0;
for (auto i : f[x]) {
while (i.l < l) add(a[--l].se);
while (i.r > r) add(a[++r].se);
while (i.l > l) del(a[l++].se);
while (i.r < r) del(a[r--].se);
ans[i.id] -= res;
}
while (l <= r) del(a[l++].se);
}
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
return 0;
}
哪怕作为 slope trick 例题出现,也是巨好的题。
暴力 dp:设 \(f_{i,x}\) 表示子树 \(i\) 内时间全部变为 \(x\) 的最小代价,转移:
这是一个 \(\min,+\) 的转移,并且加上的是凸函数,所以可以归纳出 \(f_i(x)\) 一定是下凸的。先抛开从 \(j\) 转移到 \(i\),看看 \(g_j(x)=\min\limits_{y\le x} f_{j,y}+|w-x+y|\) 相较于 \(f_j\) 会发生怎样的变化。
令 \(h_j(y)=|w-x+y|\),设 \(f_j\) 中斜率为 \(0\) 的一段区间为 \([L,R]\):
- 最简单的情况就是 \(f_j(y)\) 和 \(h_j(y)\) 都能取到最小值,要求 \(L\le w-x\le R\),即当 \(x\in[L+w,R+w]\) 时,\(g_j(x)=f_j(L)\)。
- 然后是 \(x-w\) 很靠右的情况,满足 \(x>R+w\)。注意到 \(>R\) 的部分斜率至少为 \(1\),所以最优化 \(h_j(y)\) 不如最优化 \(f_j(y)\),也就是取 \(y=R\),此时有 \(g_j(x)=f_j(R)+x-w-R\)。
- 同理,对于 \(x-w\) 很靠左的情况,注意到 \(y\) 只能取 \(\le x\) 的值,所以当 \(x<L\) 时,同理要最优化 \(f_j(y)\),但此时只能将其最优化到 \(f_j(x)\),总转移为 \(g_j(x)=f_j(x)+w\)。
- 最后是 \(f_j(x)\) 可以直接取到最优,除去第一种就是 \(L\le x<L+w\),此时想办法在不增加 \(f_j(y)\) 的前提下减少 \(h_j(y)\),发现就应该是取 \(y=L\),有 \(g_j(x)=f_j(x)+w-x+L\)。
综合一下,总的转移式为:
尝试 slope trick,维护拐点,看这四个操作都干了些什么:1. 将 \(x<L,f_j(x)\) 向上平移 \(w\);2. 将 \(L\le x<L+w\) 向上平移 \(w\),并将斜率设为 \(-1\);3. 将原先 \([L,R]\) 一段平移到 \([L+w,R+w]\);4. 将 \(x>w+R\) 的部分斜率设为 \(1\)。
注意到拐点的意义是下一段斜率减少 \(1\),所以这一堆操作看似很复杂,实际上就是将原先 \(\le L\) 的拐点都弹出来,然后加入 \(L+w,R+w\) 两个拐点,画画图应该就明白了。
然后还有合并。用可并堆来维护拐点,所有的 \(j\) 求和加到 \(i\) 发现其实就是将两个堆合并起来。关于如何找 \(L,R\),发现每一次变成 \(g\) 后只会加入一段斜率 \(>0\) 的段,所以恰好弹 \(i\) 的子树个数减 \(1\) 就能得到 \(R\),再弹一次就能得到 \(L\),并且观察到这正好符合我们的需求,一切都是那么刚好。
最后求答案,知道 \(f_1(0)=\sum w\),然后就可以推了。
#include <bits/stdc++.h>
#define int long long
#define ls(k) t[k].ls
#define rs(k) t[k].rs
using namespace std;
const int N = 6e5 + 5;
struct node { int ls, rs, dis, w; } t[N];
int ts, fa[N], w[N], cnt[N], rt[N];
inline int newnode(int x) {
return t[++ts] = {0, 0, 0, x}, ts;
}
inline int merge(int x, int y) {
if (!x || !y) return x | y;
if (t[x].w < t[y].w) swap(x, y);
rs(x) = merge(rs(x), y);
if (t[rs(x)].dis > t[ls(x)].dis) swap(ls(x), rs(x));
return t[x].dis = t[rs(x)].dis + 1, x;
}
inline void pop(int &x) { x = merge(ls(x), rs(x)); }
inline void push(int &x, int y) { x = merge(x, newnode(y)); }
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m, ans = 0; cin >> n >> m;
for (int i = 2; i <= n + m; i++)
cin >> fa[i] >> w[i], cnt[fa[i]]++, ans += w[i];
for (int i = n + m; i > 1; i--) {
while (cnt[i] > 1) pop(rt[i]), cnt[i]--;
int R = t[rt[i]].w; pop(rt[i]);
int L = t[rt[i]].w; pop(rt[i]);
push(rt[i], L + w[i]), push(rt[i], R + w[i]);
rt[fa[i]] = merge(rt[fa[i]], rt[i]);
}
while (cnt[1]--) pop(rt[1]);
while (rt[1]) ans -= t[rt[1]].w, pop(rt[1]);
cout <<ans << '\n';
return 0;
}
做 ynoi 实况:调完正确性卡空间,卡完空间卡时间,卡完时间调正确性。
还是那句话,都众数了,还是 ynoi,能不是根号复杂度?5e5 已成经典根号复杂度.jpg。
设 \(x\) 的出现次数为 \(cnt_x\),固定 \(x\) 为中心,枚举 \(x\) 的出现次数为 \(y\),那么 \(x\) 出现 \(y\) 次且合法的区间一定是一段区间,直接记一下这段区间,而总的这样区间段数的 \(O(\sum cnt_x)=O(n)\) 的,所以可以先求出所有这样的区间然后去处理询问。
不难想到用二分 + 分块求区间众数做到 \(O(n\sqrt n\log n)\),但能过才怪了。区间众数只能做到 \(O(\sqrt n)\),所以二分是不可行的。换一种思路,\(\sum cnt_x=n\),所以考虑根号分治。
对于 \(cnt_x\ge B\) 的点,直接暴力扩展,复杂度为 \(O(\frac{n^2}{B})\);对于 \(cnt_x\le B\),枚举众数的出现次数 \(y\) ,注意到 \(y\le cnt_x<B\) 时 \(x\) 才能为中心,所以总的中心数仍然是 \(O(n)\) 的,此时就可以上二分,因为 \(y\) 是固定值,所以可以预处理每个点往后区间众数次数不超过 \(y\) 的最大右端点 \(mx_i\),二分 check \(mid\) 就是判断 \(mx_{i-mid+1}\) 是否 \(\ge i+mid\)。
然后已经处理出了这些区间,考虑算答案。按中心点在询问的中点左边还是右边分类,就变成了一个简单的二维数点问题,这部分不是瓶颈。理论最优 \(B\) 取 \(\sqrt n\),复杂度 \(O(n\sqrt n+(n+m)\log n)\),实际上 \(B\) 取一百多最快,需要卡常卡空间。
拼劲全力卡到总时间 41s,直接被 gemini 卡到 25s 薄纱,人类一败涂地。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
const int B = 140;
typedef long long ll;
#define getchar() (S==TT&&(TT=(S=fsr)+fread(fsr,1,1<<15,stdin),S==TT)?EOF:*S++)
char fsr[1<<15],*S=fsr,*TT=fsr;
inline int read() {
int w = 1, q = 0; char ch = ' ';
while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
if (ch == '-') w = -1, ch = getchar();
while (ch >= '0' && ch <= '9') q = q * 10 + ch - '0', ch = getchar();
return w * q;
}
void write(ll x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
struct BIT {
ll c1[N], c2[N];
void clear() {
memset(c1, 0, sizeof(c1)), memset(c2, 0, sizeof(c2));
}
inline void upd(int x, ll v) {
for (int i = x; i < N; i += i & -i)
c1[i] += v, c2[i] += x * v;
}
inline ll qry(int x, ll c[]) {
ll res = 0;
for (; x; x -= x & -x) res += c[x];
return res;
}
inline void upd(int l, int r, int v) { upd(l, v), upd(r + 1, -v); }
inline ll qry(int l, int r) {
return qry(r, c1) * (r + 1) - qry(r, c2) - qry(l - 1, c1) * l + qry(l - 1, c2);
}
} T;
int n, a[N], cnt[N], s[N],ts;
struct node { int pos, l, r; } o[N];
inline void solve1(int x) {
int up = min(x, n - x + 1), mx = 0, cnt = 0, now = ts;
for (int i = 1; i <= up; i++) {
int j = x - i + 1, k = x + i - 1;
cnt += a[j] == x, mx = max(mx, ++s[a[j]]);
if (j ^ k) cnt += a[k] == x, mx = max(mx, ++s[a[k]]);
if (cnt >= mx) {
if (ts ^ now && o[ts].r == i - 1) o[ts].r = i;
else o[++ts] = {x, i, i};
}
}
for (int i = x - up + 1; i <= x + up - 1; i++) s[a[i]] = 0;
}
vector <int> p[N], dis[N];
int pos[N];
inline void solve2(int x) {
s[n + 1] = n + 1;
for (int i = n; i; i--) {
s[i] = s[i + 1];
if (pos[i] + x < p[a[i]].size())
s[i] = min(s[i], p[a[i]][pos[i] + x]);
}
for (int i = 1; i <= n; i++) {
if (cnt[i] >= B || cnt[i] < x) continue;
int l = dis[i][x - 1], r = min(i, n - i + 1), res = -1;
if (cnt[i] ^ x) r = min(r, dis[i][x] - 1);
while (l <= r) {
int mid = l + r >> 1;
if (s[i - mid + 1] >= i + mid) res = mid, l = mid + 1;
else r = mid - 1;
}
if (~res) o[++ts] = {i, dis[i][x - 1], res};
}
}
struct Query { int pos, l, r, id; short op; } f[N << 2];
int L[N], R[N];
ll ans[N];
int main() {
int m; n = read(), m = read();
for (int i = 1; i <= n; i++) {
a[i] = read(), cnt[a[i]]++, dis[a[i]].push_back(abs(i - a[i]) + 1);
p[a[i]].push_back(i), pos[i] = p[a[i]].size() - 1;
}
for (int i = 1; i <= n; i++) sort(dis[i].begin(), dis[i].end());
for (int i = 1; i < min(B, n); i++) solve2(i);
for (int i = 1; i <= n; i++) {
dis[i].clear(), dis[i].shrink_to_fit();
p[i].clear(), p[i].shrink_to_fit();
}
memset(s, 0, sizeof(s));
for (int i = 1; i <= n; i++)
if (cnt[i] >= B) solve1(i);
int len = 0;
for (int i = 1; i <= m; i++) {
int l = read(), r = read();
int mid = l + r >> 1; L[i] = l, R[i] = r;
f[++len] = {r, mid + 1, r, i, 1};
f[++len] = {mid, mid + 1, r, i, -1};
}
sort(o + 1, o + 1 + ts, [](node x, node y) {
return x.pos < y.pos;
});
sort(f + 1, f + 1 + len, [](Query x, Query y) {
return x.pos < y.pos;
});
for (int i = 1, j = 0, k = 0; i <= n; i++) {
while (j < ts && o[j + 1].pos <= i)
j++, T.upd(i + o[j].l - 1, i + o[j].r - 1, 1);
while (k < len && f[k + 1].pos <= i)
k++, ans[f[k].id] += T.qry(f[k].l, f[k].r) * f[k].op;
}
len = 0; T.clear();
for (int i = 1; i <= m; i++) {
int l = L[i], r = R[i], mid = l + r >> 1;
f[++len] = {mid, l, mid, i, 1};
if (l > 1) f[++len] = {l - 1, l, mid, i, -1};
}
sort(f + 1, f + 1 + len, [](Query x, Query y) {
return x.pos < y.pos;
});
for (int i = 1, j = 0, k = 0; i <= n; i++) {
while (j < ts && o[j + 1].pos <= i)
j++, T.upd(i - o[j].r + 1, i - o[j].l + 1, 1);
while (k < len && f[k + 1].pos <= i)
k++, ans[f[k].id] += T.qry(f[k].l, f[k].r) * f[k].op;
}
for (int i = 1; i <= m; i++) write(ans[i]), putchar('\n');
return 0;
}
比较经典?
容易想到,先建边然后跑拓扑排序,关键就在于怎么建 \(B\to A\) 的边。
这题的 \(A,B\) 都是通过 \(S\) 的子串给出的,所以不能依赖于字符串总长。
对于这个串本身去做,那么就是 SA/SAM,肯定先尝试简单的 SA。
根据经典结论,\(B\) 的前缀一定是为一段排名的后缀的前缀的 \(A\) ,那不是直接二分出来这个区间,然后 st 表优化建图就做完了?
欸但是题目中特殊性质是啥玩意?不对,后缀的话可能会在后面凭空多一些,所以还要求 \(|A|\ge |B|\)。但仔细思考一下,直接将 \(A\) 排序后连边就可以了,不要忘掉一些最简单的思路。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5 + 5, M = 20;
int n;
struct SA {
int cnt[N], rk[N], sa[N], id[N], t[N << 1], h[N], f[N][M];
inline bool cmp(int x, int y, int len) {
return t[x] == t[y] && t[x + len] == t[y + len];
}
inline void init(string s) {
int m = 200;
for (int i = n + 1; i <= n + n; i++) t[i] = 0;
for (int i = 1; i <= m; i++) cnt[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 - len + 1; i <= n; 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];
for (int i = 1; i <= n; i++) t[i] = rk[i]; m = 0;
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]] = f[rk[i]][0]= H;
}
for (int j = 1; j < M; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
f[i][j] = min(f[i][j - 1], f[i + (1 << j - 1)][j - 1]);
}
inline int qry(int l, int r) {
int k = __lg(r - l + 1);
return min(f[l][k], f[r - (1 << k) + 1][k]);
}
inline int lcp(int x, int y) {
if (x == y) return n - x + 1;
x = rk[x], y = rk[y]; if (x > y) swap(x, y);
return qry(x + 1, y);
}
} S;
string s;
struct node { int l, r, len, id; } a[N], b[N];
inline bool cmp(node x, node y) {
int l = S.lcp(x.l, y.l);
if (l >= min(x.len, y.len)) return x.len < y.len;
else return s[x.l + l] < s[y.l + l];
}
int f[N][M], ts, deg[N << 5], g[N << 5], L[N];
vector <int> p[N << 5];
inline void add(int x, int y) {
p[x].push_back(y), deg[y]++;
}
inline void ins(int x, int l, int r) {
int k = __lg(r - l + 1); add(x, f[l][k]);
add(x, f[r - (1 << k) + 1][k]);
}
inline void solve() {
cin >> s; n = s.size(), s = " " + s; S.init(s);
int na; cin >> na;
for (int i = 1; i <= na; i++) {
cin >> a[i].l >> a[i].r;
a[i].len = a[i].r - a[i].l + 1, a[i].id = i;
}
sort(a + 1, a + 1 + na, cmp);
int nb; cin >> nb; ts = na + nb;
for (int i = 1; i <= nb; i++) {
cin >> b[i].l >> b[i].r;
b[i].len = b[i].r - b[i].l + 1, b[i].id = i;
}
for (int i = 1; i <= na; i++) {
f[i][0] = a[i].id, L[a[i].id] = a[i].len;
}
for (int j = 1; j < M; j++)
for (int i = 1; i + (1 << j) - 1 <= na; i++) {
f[i][j] = ++ts; add(f[i][j], f[i][j - 1]);
add(f[i][j], f[i + (1 << j - 1)][j - 1]);
}
int _; cin >> _;
while (_--) {
int x, y; cin >> x >> y;
add(x, y + na);
}
for (int i = 1; i <= nb; i++) {
int l = 1, r = na, ansl = 0;
while (l <= r) {
int mid = l + r >> 1;
if (cmp(a[mid], b[i]))
ansl = mid, l = mid + 1;
else r = mid - 1;
}
ansl++;
if (ansl > na || S.lcp(b[i].l, a[ansl].l) < b[i].len) continue;
l = ansl, r = na; int ansr = 0;
while (l <= r) {
int mid = l + r >> 1;
if (S.lcp(a[mid].l, b[i].l) >= b[i].len)
ansr = mid, l = mid + 1;
else r = mid - 1;
}
ins(i + na, ansl, ansr);
}
queue <int> q;
for (int i = 1; i <= ts; i++)
if (!deg[i]) q.push(i);
while (q.size()) {
int t = q.front(); q.pop();
if (t <= na) g[t] += L[t];
for (auto i : p[t]) {
g[i] = max(g[i], g[t]);
if (!--deg[i]) q.push(i);
}
}
int ans = 0, fl = 0;
for (int i = 1; i <= ts; i++) {
if (deg[i]) fl = 1;
ans = max(ans, g[i]);
}
cout << (fl ? -1 : ans) << '\n';
for (int i = 1; i <= ts; i++)
p[i].clear(), deg[i] = g[i] = 0;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T--) solve();
return 0;
}
2026.7.30 day 10 复杂贪心
其实是杂题选讲,但选的题都比较小清新。P6631 是纯粹的贪心好题。
大部分题目的题解:
比较小清新,但是直接被普及组部分创飞了。
第一反应是按左端点排序每个点到达的是区间是一个区间,但是发现如果存在包含的情况就坏了,但是包含的一定最小的限制最强,所以只保留被包含的即可。
点也排序,然后容易设计 dp:\(f_{i,j}\) 表示前 \(i\) 个,当前访问到的最右区间为 \(j\),。看似是 \(O(nm)\) 的,但是注意到 \(i\) 一定不会跨过 \(a_{i+1}\),去掉本身就有点存在的区间,除了平凡的只向左走,所以 \(r_j\in [a_i,a_{i+1})\),所以有用的状态数总和其实是 \(O(m)\) 的。
然后观察 \(f_{i-1,k}\) 转移到 \(f_{i,j}\) 时的代价,确定了 \(i\) 和 \(i-1\) 是先往左再往右还是先往右再往左时,与 \(k\) 和 \(j\) 的两部分其实是独立的,可以直接拆开,所以再记一个当前轨迹是哪种,转移的枚举 \(k,j\) 就是独立的,复杂度就变成了 \(O(n+m)\)。
实际上状态里不用记 \(j\),直接在转移的时候枚举一下 \(i-1\) 完成的最右区间即可。注意区间去包含以及去掉本身被点覆盖的区间的过程不要写错!!!
注意转移中的独立性,然后拆贡献。
#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 = 2e5 + 5;
const int INF = 1e18;
pii b[N];
int a[N], f[N][2];
bool vis[N];
inline void chmin(int &x, int y) {
if (x > y) x = y;
}
bool operator <(pii x, pii y) {
if (x.fi ^ y.fi) return x.fi < y.fi;
else return x.se > y.se;
}
inline void solve() {
int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
for (int i = 1; i <= m; i++)
cin >> b[i].fi >> b[i].se, vis[i] = false;
sort(b + 1, b + 1 + m); int ts = 0;
for (int i = 1; i <= m; i++) {
while (ts && b[ts].se >= b[i].se) ts--;
b[++ts] = b[i];
}
m = ts;
for (int i = 1, j = 1; i <= n; i++) {
while (j <= m && b[j].se < a[i]) j++;
while (j <= m && b[j].fi <= a[i]) vis[j++] = true;
}
ts = 0;
for (int i = 1; i <= m; i++)
if (!vis[i]) b[++ts] = b[i];
m = ts; a[0] = -1e12, a[n + 1] = 1e12;
for (int i = 1; i <= n + 1; i++)
f[i][0] = f[i][1] = INF;
for (int i = 1, j = 1; i <= n + 1; i++) {
vector <int> A, B; A.push_back(a[i - 1]);
while (j <= m && b[j].se < a[i])
A.push_back(b[j].fi), B.push_back(b[j].se), j++;
B.push_back(a[i]);
for (int k = 0; k < A.size(); k++) {
int dis1 = A[k] - a[i - 1], dis2 = a[i] - B[k];
for (int x = 0; x < 2; x++)
for (int y = 0; y < 2; y++)
chmin(f[i][y], f[i - 1][x] + dis1 * (1 + x) + dis2 * (1 + !y));
}
}
cout << f[n + 1][0] << '\n';
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int T; cin >> T;
while (T--) solve();
return 0;
}
有点不好评价,可能算是诈骗题。
\(\times a_i\) 要注意了,答案保证 \(\le 2\times 10^9\),把 \(\times 1\) 丢掉,剩下的就不超过 \(30\) 个。
但是直接枚举哪个要不要选还是很蠢。注意到相同的值一定动的是位置靠前的一个前缀,那么这样暴搜的量就可以降到据说是 \(<1.5\times 10^4\)。
枚举出来后,对于加法选在代价内且对答案增加最多的,但是每次遍历一遍还是会炸。再贪一点,在相邻两个乘号之间移动的一定是从大到小的一段前缀。直接求不大好求(因为要满足代价的限制,并且每一段贡献系数不同)。但是可以二分一下对答案的增加量 \(x\),判断增加量 \(\ge x\) 的代价是否小于 \(w'\) 即可。
#include <bits/stdc++.h>
#define pii pair <int, int>
#define fi first
#define se second
#define int long long
using namespace std;
const int M = 40;
const int INF = 1e18;
int n, W, A, B, pre[M], m, a[M], ans, prod = 1;
unordered_map <int, int> now;
vector <int> p[M], s[M];
bool vis[M];
inline pii chk(int x) {
int tmp = prod, cnt = 0, sum = 0;
for (int i = 0; i <= m; i++) {
if (i && !vis[i]) tmp /= a[i];
int l = 0, r = (int)p[i].size() - 1, res = -1;
while (l <= r) {
int mid = l + r >> 1;
if (p[i][mid] * (prod - tmp) >= x)
res = mid, l = mid + 1;
else r = mid - 1;
}
if (~res)
cnt += res + 1, sum += s[i][res] * (prod - tmp);
}
return {cnt, sum};
}
inline int solve(int W) {
int l = 0, r = INF, res = INF, tr = prod; W /= A;
for (int i = 0, t = prod; i <= m; i++) {
if (i && !vis[i]) t /= a[i];
if (s[i].size()) tr += s[i].back() * t;
}
while (l <= r) {
int mid = l + r >> 1;
if (chk(mid).fi <= W) res = mid, r = mid - 1;
else l = mid + 1;
}
pii t = chk(res - 1);
return tr + t.se - max(t.fi - W, 0ll) * (res - 1);
}
void dfs(int k, int s) {
if (s < 0) return;
if (k > m) return ans = max(ans, solve(s)), void();
if (!pre[k] || vis[pre[k]]) vis[k] = true, dfs(k + 1, s - B);
vis[k] = false, dfs(k + 1, s);
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int W; cin >> n >> W >> A >> B; ans = 1;
for (int i = 1; i <= n; i++) {
char op; int x; cin >> op >> x;
if (op == '*') {
if (x == 1) continue;
a[++m] = x, pre[m] = now[x], now[x] = m;
ans *= x, prod *= x;
} else p[m].push_back(x), ans += x;
}
for (int i = 0; i <= m; i++) {
int len = p[i].size(); s[i].resize(len);
sort(p[i].begin(), p[i].end(), greater <int>());
for (int j = 0; j < len; j++)
s[i][j] = (j ? s[i][j - 1] : 0) + p[i][j];
}
dfs(1, W), cout << ans << '\n';
return 0;
}
结论比较好猜,维护比较有意思。
\(a<b<c<d\),不难证最优的一定是 \(ab+cd\)。所以可以得到贪心算法,注意还得实时保证不存在自环和独立的联通块:从大往小,每次往之前形成的图中加边,在合法的前提下尽量连较大的边。
看似这样的结构比较复杂,但发现自环是简单的,联通块不独立即如果一个联通块只有一条出边那么这条出边一定要和一定连完仍有出边的联通块相连,除了最后的时候。
于是每个时候,要么剩一个出边 \(>1\) 的联通块,要么剩若干个出边 \(=1\) 的联通块。每次直接把当前点加入前者(没有就直接当成就是这个点),然后尽可能和 \(=1\) 的合并直到不存在后者;或者其出边数也 \(=1\) 了,把它扔到后面一种去就可以了。
实现需要桶排,具体过程用两个队列即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e7 + 5, V = 5e5 + 5;
unsigned seed;
inline unsigned rnd(unsigned x){
x ^= x << 13; x ^= x >> 17;
x ^= x << 5; return x;
}
inline int rad(int x, int y){
seed = rnd(seed);
return seed % (y - x + 1) + x;
}
struct node { int d, v; } a[N], b[N];
int cnt[V];
queue <int> q1, q2;
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int _, n; cin >> _ >> n;
if (!_) {
for (int i = 1; i <= n; i++) cin >> b[i].d;
for (int i = 1; i <= n; i++) cin >> b[i].v;
} else {
cin >> seed;
for (int i = 1; i <= n; i++)
b[i].d = 1, b[i].v = rad(1, 500000);
for (int i = 1; i < n - 1; i++) b[rad(1, n)].d++;
}
for (int i = 1; i <= n; i++) cnt[b[i].v]++;
for (int i = 1; i < V; i++) cnt[i] += cnt[i - 1];
for (int i = 1; i <= n; i++) a[cnt[b[i].v]--] = b[i];
ll ans = 0;
for (int i = n; i; i--) {
if (q1.empty()) {
for (int j = 1; j <= a[i].d; j++)
q1.push(a[i].v);
} else {
ans += 1ll * q1.front() * a[i].v, q1.pop();
for (int j = 1; j < a[i].d; j++) q1.push(a[i].v);
}
while (q1.size() > 1 && !q2.empty())
ans += 1ll * q1.front() * q2.front(), q1.pop(), q2.pop();
if (q1.size() == 1) q2.push(q1.front()), q1.pop();
}
int x = q2.front(); q2.pop(); int y = q2.front();
cout << ans + 1ll * x * y << '\n';
return 0;
}
CF1592F1 Alice and Recoloring 1 & CF1592F2 Alice and Recoloring 2
有趣的题目,但本质上还是考察 trick 理解度。
先看 F1。发现这个操作 2 和操作 3 就是在扯淡,只有操作 1 和 4 有用。
矩阵的形式不好刻画,尝试转化成单点。 类比二维差分,设 \(b_{i,j}=a_{i,j}\oplus a_{i+1,j}\oplus a_{i,j+1}\oplus a_{i+1,j+1}\),那么操作一就是翻转 \(b_{x,y}\),操作四就是翻转 \(b_{x-1,y-1},b_{x-1,m},b_{n,y-1}\) 和 \(b_{n,m}\)。
注意到操作四是用 3 的代价翻转四个,并且有一个是 \(b_{n,m}\),所以最多进行一次,否则相当于 6 的代价翻转六个不如直接上操作一,且这些原先必须都为黑色。
于是 F1 就是检查是否存在这样的 \(x,y\),是就是 \(b\) 黑色数量减一,否则就是黑色数量。
容易有的一个错误思路:设 \(b_{i,j}=b_{i-1,j-1}\oplus b_{i-1,j}\oplus b_{i,j-1}\oplus b_{i,j}\)。但是这样发现 1 的代价对应四个,3 的代价对应 1 个,显然不太好做,所以考虑尽量将操作实际的影响与代价匹配。
接下来是 F2,显然操作四只会进行一次的性质消失了。
但还是能观察出一些新的性质:
- \(x,y\) 两两不同,否则相当于 4 的代价操作四个。
- \(a_{x-1,m},a_{n,y-1},a_{x-1,y-1}\) 必须都为 \(1\),因为不为 1 的话至少还得花一次将由 0 变 1 的那个变回来(由上一条性质得,一定不会再由上一条来改变),也不优。
满足性质二就会更优。将可以的 \(x,y\) 连边,性质一的限制相当于求二分图最大匹配,然后做完了。这题比较善良匈牙利三次方可过。
F1:
#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int a[N][N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m, ans = 0; cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
char x; cin >> x; a[i][j] = (x == 'B');
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
ans += (a[i][j] ^= a[i + 1][j] ^ a[i][j + 1] ^ a[i + 1][j + 1]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if (a[n][m] && a[i - 1][m] && a[n][j - 1] && a[i - 1][j - 1])
return cout << ans - 1 << '\n', 0;
cout << ans << '\n';
return 0;
}
F2:
#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int a[N][N], b[N];
bool vis[N];
vector <int> p[N];
int dfs(int k) {
vis[k] = 1;
for (auto i : p[k])
if (!b[i] || !vis[b[i]] && dfs(b[i]))
return b[i] = k, 1;
return 0;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m, ans = 0; cin >> n >> m;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
char x; cin >> x;
a[i][j] = x == 'B';
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
a[i][j] ^= a[i + 1][j] ^ a[i][j + 1] ^ a[i + 1][j + 1];
for (int i = 1; i < n; i++)
for (int j = 1; j < m; j++)
if (a[i][j] && a[i][m] && a[n][j]) p[i].push_back(j);
int cnt = 0;
for (int i = 1; i <= n; i++)
memset(vis, 0, sizeof(vis)), cnt += dfs(i);
a[n][m] ^= cnt & 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) ans += a[i][j];
cout << ans - cnt << '\n';
return 0;
}
CF1446D1 Frequency Problem & CF1446D2 Frequency Problem
见识过 ynoi 后,就会发现根本不是难题,而且有 D1 后 D2 难度下了一个台阶。
全局众数不唯一则答案为 \(n\),否则全局众数一定是答案区间的众数,否则可以直接调整。
于是 D1 就是简单的,枚举另一个众数,然后类似 P8330 那样,设一个为 \(1\),一个为 \(-1\),就是求最长的和为 \(0\) 的区间长度。注意到若区间内存在更多的数也可以调整成更优,不用管。
但是这也太暴力了,众数优先尝试根号。根号分治,出现次数 \(\ge B\) 的暴力做,\(<B\) 的枚举出现次数(经典做法),然后枚举右端点 \(i\) ,最可能合法的显然是 \([i,j]\) 所有数出现次数不超过 \(x\) 的最小 \(j\),于是双指针动态维护一下即可。
简单取 \(B=\sqrt n\),复杂度 \(O(n\sqrt n)\)。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
const int B = 500;
int a[N], b[N], s[N], n, ans, mx[N];
vector <int> p[N];
inline void solve1(int x) {
for (auto i : p[x]) b[i] = -1;
for (int i = 0; i <= 2 * n; i++) mx[i] = -1;
mx[n] = 0;
for (int i = 1, s = 0; i <= n; i++) {
s += b[i];
if (~mx[s + n]) ans = max(ans, i - mx[s + n]);
else mx[s + n] = i;
}
for (auto i : p[x]) b[i] = 0;
}
int cnt[N];
inline void solve2(int x) {
int c = 0;
for (int i = 1, j = 1; i <= n; i++) {
if (++cnt[a[i]] == x) c++;
while (cnt[a[i]] > x)
c -= cnt[a[j]]-- == x, j++;
if (c >= 2) ans = max(ans, i - j + 1);
}
for (int i = 1; i <= n; i++) cnt[i] = 0;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i], p[a[i]].push_back(i);
int mx = 0;
for (int i = 1; i <= n; i++)
if (p[i].size() > p[mx].size()) mx = i;
for (int i = 1; i <= n; i++)
if (p[i].size() == p[mx].size() && i ^ mx)
return cout << n << '\n', 0;
for (auto i : p[mx]) b[i] = 1;
for (int i = 1; i <= n; i++)
if (p[i].size() >= B && i ^ mx) solve1(i);
for (int i = 1; i < B; i++) solve2(i);
cout << ans << '\n';
return 0;
}
同样是有点意思的思维题。
发现若 \(i\) 被保留,则类似一个传递的关系:若某一个时刻要吃 \((i,j)\),则往前 \(j\) 和 \(i\) 地位相同也要被保留,再出现 \((j,k)\) 则 \(k\) 也要被保留……以此类推。
从后往前递推,就可以得出若要保留 \(i\),则一定要留着 \(j\) 的集合 \(S_i\),但是 \(j\) 的作用是给 \(i\) 当垫背的被吃掉。“留着你是因为以后要炖了你。”
然后考虑如何判断 \((i,j)\) 是否能同时保留。一个想法是类似一个的情况,不过初始的是 \(i,j\) 都要被保留,复杂度 \(O(n^2m)\)。但是每次都做一遍有点浪费,尝试将两部分拼起来。
发现 \(S_i\) 中的全部要留给 \(i\),\(S_j\) 中的全部要留给 \(j\),所以理应 \(S_i\cap S_j=\varnothing\),不然就会出现留给一个的被另一个用掉了,并且此时两者的过程相当于独立开来了,所以一定可行。
复杂度可以用 bitset 做到 \(O(nm+\frac{n^3}{w})\),但用没用都能过。
#include <bits/stdc++.h>
using namespace std;
const int N = 405, M = 1e5 + 5;
bitset <N> f[N];
int a[M], b[M];
bool ans[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int n, m; cin >> n >> m;
for (int i = 1; i <= m; i++)
cin >> a[i] >> b[i];
for (int i = 1; i <= n; i++) {
f[i][i] = 1;
for (int j = m; j; j--) {
bool x = f[i][a[j]], y = f[i][b[j]];
if (x && y) { ans[i] = 1; break; }
else if (x || y) f[i][a[j]] = f[i][b[j]] = 1;
}
}
int cnt = 0;
for (int i = 1; i <= n; i++) {
if (ans[i]) continue;
for (int j = i + 1; j <= n; j++)
cnt += !ans[j] && (f[i] & f[j]).none();
}
cout << cnt << '\n';
return 0;
}
这才是真正的贪心题啊。
一个转化的想法时进行 \(x\) 次操作一后代价变为 \(x+\sum \max(a_i-a_{i-2},0)\),这个意识是好的,但很难进一步操作。有时候不一定是很难的算法,想想最简单的贪心怎么样!
设操作完 \([1,i]\) 后能往后贡献的有 \(f_i\) 个操作 \(1\) 和 \(g_i\) 个操作二/三。首先肯定能继承就继承,不行了再新建,然后在满足这个的前提下要对后面更优。
- 下一个至少需要 \(t=\max(a_{i+1}-g_{i-1},0)\) 个操作一,于是先继承 \(\min(t,a_i,f_{i-1})\) 个(后两个是限制性的,略去不讲)。
- 然后此时继承操作一没用了,尽量都继承操作二,\(g_i\leftarrow \min(g_{i-1},a_i-f_i)\)。
- 若此时 \(a_i=f_i+g_i\),则直接跳过。否则此时 \(g_i\) 一定等于 \(g_{i-1}\),尝试继承 \(f\):\(f_i\) 加上 \(\min(a_i-f_i-g_i,f_{i-1}-f_i)\)。
- 若还不够,那就要新建了,\(ans\) 增加 \(a_i-f_i-g_i\)。
- 具体怎么新建呢?若此时继承操作一还能有贡献就加操作一,一定不劣:\(f_i\) 加上 \(\min(a_i-f_i-g_i,\max(0,a_{i+1}-f_i-g_{i-1}))\)。
- 没加够剩下的就补 \(g\) 了,\(g_i\) 加上 \(a_i-f_i-g_i\)。
PS:以上过程中 \(f,g\) 都是动态更新的,即上一步对 \(f,g\) 的修改会影响到之后。
有的时候贪心的思想其实没有那么困难,但是需要细心的分讨以及严谨的思考。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 10;
int a[N], f[N], g[N];
inline void solve() {
int n, ans = 0; cin >> n; n += 2;
for (int i = 3; i <= n; i++) cin >> a[i];
a[n + 1] = a[n + 2] = 0;
for (int i = 3; i <= n; i++) {
f[i] = min({a[i], f[i - 1], max(a[i + 1] - g[i - 1], 0ll)});
g[i] = min(g[i - 2], a[i] - f[i]);
f[i] += min(a[i] - f[i] - g[i], f[i - 1] - f[i]);
ans += a[i] - f[i] - g[i];
f[i] += min(a[i] - f[i] - g[i], max(a[i + 1] - g[i - 1] - f[i], 0ll));
g[i] += a[i] - f[i] - g[i];
}
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;
}


浙公网安备 33010602011771号