做题笔记 02
1. P13834 【MX-X18-T6】「FAOI-R6」Voices of the Chord
https://www.luogu.com.cn/problem/P13834。
题意:给定长度为 \(k\) 的正整数序列 \(b\),长度为 \(m\) 的集合序列 \(S\),\(S_i\) 内元素互不相同。在线维护长度为 \(n\) 的正整数序列 \(a\),支持操作:1. 给定 \(l,r,x\),\(\forall i\in[l,r],\forall j\in S_{b_i},a_j\leftarrow a_j+x\);2. 查询 \(\sum_{i\in[l,r]}a_i\)。
\(n,m,k,q\leq 10^5,\sum |S_i|\leq 3\times 10^5\)。
比较复杂的分块题。
Part 1. \(b\) 整块
对 \(b\) 分块,考虑整块部分,即 \(b\) 整块对 \(a\) 前缀的贡献,目标是可以 \(O(1)\) 查询。
求出 \(F_{i,j}\) 表示若一次 \(1\) 操作覆盖了 \(b\) 的第 \(i\) 个块(操作的 \(x=1\)),那么这次操作中 \(b\) 的第 \(i\) 个块对 \(a\) 前缀 \(j\) 的贡献系数。
求出 \(F\) 后整块的修改就可以绑定在 \(b\) 上了,具体的,维护 \(B_i\) 表示覆盖 \(b\) 上块 \(i\) 的 \(1\) 操作 \(x\) 的和。
然后查询 \(a_{[l,r]}\) 的时候就可以枚举 \(b\) 中的块 \(i\),然后累加 \((F_{i,r}-F_{i,l-1})B_i\)。
最后的问题是 \(F\) 怎么求。直接求的话复杂度容易爆炸,正确的做法是:枚举 \(b\) 块 \(i\),求出块内对于每个 \(j\),集合 \(S_j\) 出现了几次,然后枚举 \(j\in[1,m]\),\(S_j\) 内的元素,乘这个集合的出现次数,贡献到 \(a\) 对应位置,最后前缀和一下即可。
Part 2. \(b\) 散块
设 \(T_i\) 表示 \(i\) 出现在了哪些 \(S\) 中,\(T_i=\{j|i\in S_j\}\)。容易得到 \(\sum |T|=\sum |S|\),是可以接受的。
此时的一个可行的做法是:将修改绑定到 \(S\) 上。那么现在的修改和查询形如:
- \(\forall i\in[l,r], S_{b_i}\leftarrow S_{b_i}+x\);
- 求 \(\sum_{i\in[l,r]}\sum_{j\in T_i}S_j\)。
把 \(T\) 按照下标顺序合并为一个数组,查询容易改为:
- 求 \(\sum_{i\in[l,r]} S_{a_j}\)。
这个问题就比较可做了。具体的,首先按照 \(a\) 分块,然后还是考虑
\(b\) 的区间修改对 \(a\) 整块/散块的贡献。
此时修改操作的性质有:每次修改一段 \(b\) 下标连续段,长度小于块长。
Part 3. \(a\) 整块
求出 \(G_{i,j}\) 数组表示若进行一次 \(\forall i\in[1,j],S_{b_i}\leftarrow S_{b_i}+1\) 操作,\(a\) 的第 \(i\) 块的 \(\sum_{k\in Block(i)}S_{a_{k}}\),求的方法与 \(F\) 类似,同样注意不要使得复杂度退化。
然后每次修改时,可以直接枚举 \(a\) 的块然后贡献过去,具体的,修改 \(l,r,x\) 就可以枚举 \(i\),然后对 \(a\) 的第 \(i\) 块贡献 \(x(G_{i,r}-G_{i,l-1})\)。
Part 4. \(a\) 散块
注意到此时是 \(b\) 散块对 \(a\) 散块贡献,直接记录即可。
//P13834
#include <bits/stdc++.h>
using namespace std;
const int B = 1000;
int n, K, m, q, b[100010], a[300010];
vector<int> S[100010], T[100010];
unsigned int F[400][100010], G[400][100010];
int Binb[100010], Ble[100010], Bri[100010];
int le[300010], ri[300010];
int Ainb[300010], Ale[300010], Ari[300010];
unsigned int SS[100010], BB[400], AA[400];
int Scnt[100010];
int main(){
scanf("%d%d%d%d", &n, &K, &m, &q);
for(int i = 1; i <= K; ++ i){
scanf("%d", &b[i]);
Binb[i] = (i - 1) / B + 1;
if(Binb[i] != Binb[i-1]){
Bri[Binb[i-1]] = i-1;
Ble[Binb[i]] = i;
}
}
Bri[Binb[K]] = K;
for(int i = 1; i <= m; ++ i){
int sz;
scanf("%d", &sz);
for(int j = 1; j <= sz; ++ j){
int x;
scanf("%d", &x);
S[i].push_back(x);
T[x].push_back(i);
}
}
for(int i = 1; i <= Binb[K]; ++ i){
for(int j = Ble[i]; j <= Bri[i]; ++ j){
++ Scnt[b[j]];
}
for(int j = 1; j <= m; ++ j){
for(int k : S[j]){
F[i][k] += Scnt[j];
}
}
for(int j = 1; j <= n; ++ j){
F[i][j] += F[i][j-1];
}
for(int j = Ble[i]; j <= Bri[i]; ++ j){
-- Scnt[b[j]];
}
}
for(int i = 1, la = 0; i <= n; ++ i){
le[i] = la + 1;
ri[i] = le[i] + T[i].size() - 1;
la = ri[i];
for(int j = le[i]; j <= ri[i]; ++ j){
a[j] = T[i][j-le[i]];
}
}
for(int i = 1; i <= ri[n]; ++ i){
Ainb[i] = (i - 1) / B + 1;
if(Ainb[i] != Ainb[i-1]){
Ari[Ainb[i-1]] = i-1;
Ale[Ainb[i]] = i;
}
}
Ari[Ainb[ri[n]]] = ri[n];
for(int i = 1; i <= Ainb[ri[n]]; ++ i){
for(int j = Ale[i]; j <= Ari[i]; ++ j){
++ Scnt[a[j]];
}
for(int j = 1; j <= K; ++ j){
G[i][j] += Scnt[b[j]] + G[i][j-1];
}
for(int j = Ale[i]; j <= Ari[i]; ++ j){
-- Scnt[a[j]];
}
}
unsigned int la = 0;
while(q--){
int op, l, r;
unsigned int x;
scanf("%d%d%d", &op, &l, &r);
la &= 65535;
l ^= la;
r ^= la;
if(op == 1){
scanf("%u", &x);
if(Binb[l] == Binb[r]){
for(int i = l; i <= r; ++ i){
SS[b[i]] += x;
}
for(int i = 1; i <= Ainb[ri[n]]; ++ i){
AA[i] += x * (G[i][r] - G[i][l-1]);
}
} else {
for(int i = l; i <= Bri[Binb[l]]; ++ i){
SS[b[i]] += x;
}
for(int i = Ble[Binb[r]]; i <= r; ++ i){
SS[b[i]] += x;
}
for(int i = 1; i <= Ainb[ri[n]]; ++ i){
AA[i] += x * (G[i][Bri[Binb[l]]] - G[i][l-1]);
AA[i] += x * (G[i][r] - G[i][Ble[Binb[r]]-1]);
}
for(int i = Binb[l] + 1; i < Binb[r]; ++ i){
BB[i] += x;
}
}
} else {
la = 0;
for(int i = 1; i <= Binb[K]; ++ i){
la += BB[i] * (F[i][r] - F[i][l-1]);
}
l = le[l];
r = ri[r];
if(Ainb[l] == Ainb[r]){
for(int i = l; i <= r; ++ i){
la += SS[a[i]];
}
} else {
for(int i = l; i <= Ari[Ainb[l]]; ++ i){
la += SS[a[i]];
}
for(int i = Ale[Ainb[r]]; i <= r; ++ i){
la += SS[a[i]];
}
for(int i = Ainb[l] + 1; i < Ainb[r]; ++ i){
la += AA[i];
}
}
printf("%u\n", la);
}
}
return 0;
}
2. P13535 [IOI 2025] 纪念品(souvenirs)
https://www.luogu.com.cn/problem/P13535。
题意:交互题。有 \(n\) 件纪念品,价格分别为 \(P_0,P_1,...,P_{n-1}\),均为正整数且严格递减。交互库刚开始只给你 \(n\) 和 \(P_0\)。定义一次购买操作为:向交互库输出一个正整数 \(m\),交互库会执行:维护一个初始为空的 vector \(k\),将 \(i\) 从 \(0\) 到 \(n-1\) 遍历,若 \(m\geq P_i\),则将 \(i\) 插入 \(k\) 中,并且令 \(m\leftarrow m-P_i\),返回 \(k\) 以及最后的 \(m\) 值。你需要进行若干次购买操作,要求每次购买必须购买至少一件物品,若最后第 \(i\) 号物品恰好购买了 \(i\) 次,则通过题目(\(0\) 号物品当然只能购买 \(0\) 次)。
题目的限制其实非常严格,找到限制中唯一能走的一步跟着走就做完了。
首先,第一次询问肯定不能太小,否则买不到任何东西就寄了。于是选择询问 \(P_{0}-1\),这个数肯定不会出现问题。
然后,如果返回的那个集合中只有一个数价格没有确定,那么随之就确定了;否则有多个没有确定,可以知道他们的平均数,这个数肯定小于这个集合中价格最大的那个,考虑递归下去问这个平均数,求完这个集合内除掉最大值剩下的价格,就能确定最大值。
具体流程:定义函数 \(solve(x)\) 表示能够求得所有价格 \(\leq x\) 的纪念品的价格。初始调用 \(solve(P_0-1)\)。
- 购买 \(x\),得到一个集合 \(S\) 以及他们的平均值。
- 反复执行 3, 4 操作使得集合大小为 \(1\)。
- 将集合中那些已经确定价格的数删掉,更新平均值。
- 递归下去 \(solve\) 平均值,可以得到所有 \(\leq\) 平均值的价格,此时整个集合内已知价格一定会变多。
- 此时可以计算集合内唯一一个元素的价格(显然为价格最大的那个元素)。
- 找到后面所有的没有计算价格的元素中下标最小的一个 \(i\),调用 \(solve(P_{i-1}-1)\),因为此时 \(P_{i-1}\) 一定算过了。
可以发现,算法过程中以下标 \(i\) 作为最小下标的集合只有 \(1\) 个,所以此时每个物品的购买次数都是不大于题目要求的,然后知道所有价格后补全剩下的购买次数即可。
//P13535
#include <bits/stdc++.h>
using namespace std;
std::pair<std::vector<int>, long long> transaction(long long M);
int cnt[110], al;
typedef long long ll;
ll P[110];
void solve(ll val){
pair<vector<int>, ll> now = transaction(val);
for(auto i : now.first){
++ cnt[i];
}
vector<int> tmp;
for(auto i : now.first){
if(!P[i]){
tmp.push_back(i);
} else {
now.second += P[i];
}
}
swap(now.first, tmp);
int l = 0, r = now.first.size()-1;
while(l < r){
ll sum = val - now.second;
for(int i = r+1; i < now.first.size(); ++ i){
sum -= P[now.first[i]];
}
ll pj = sum / (r - l + 1);
solve(pj);
while(P[now.first[r]]){
-- r;
}
}
ll sum = val - now.second;
for(int i : now.first){
sum -= P[i];
}
P[now.first[0]] = sum;
for(int i = now.first[0] + 1; i < al; ++ i){
if(!P[i]){
solve(P[i-1] - 1);
break;
}
}
}
void buy_souvenirs(int N, ll P0){
al = N;
P[0] = P0;
cnt[0] = 0;
for(int i = 1; i < N; ++ i){
P[i] = cnt[i] = 0;
}
solve(P0 - 1);
for(int i = 1; i < N; ++ i){
while(cnt[i] < i){
transaction(P[i]);
++ cnt[i];
}
}
}
3. P13537 [IOI 2025] 世界地图(worldmap)
https://www.luogu.com.cn/problem/P13537。
给你一张 \(n\) 个点的无向连通图,要求构造一个 \(2n\times 2n\) 的矩形,每个元素 \(\in[1, n]\),要求:\(\forall u, v\in[1, n],u\neq v\),图中存在边 \((u,v)\) 等价于矩形中有两个边相邻的位置一个是 \(u\),一个是 \(v\)。
思路是现提取生成树,然后再塞进去非树边。
dfs 生成树性质是好的:如果把非树边绑到浅节点上,叶子节点不会有非树边,非叶子节点的非树边个数小于 \(siz\)。
构造方案:
(最上面的 5 应该改为 4)
红色位置可以任意插入非树边。
//P13537
#include <bits/stdc++.h>
using namespace std;
vector<int> g[45], son[45];
int dfn[45], siz[45], dfc, st[45], stc, vis[45], fdf[45], dep[45];
void dfs(int x, int fa){
dep[x] = dep[fa] + 1;
vis[x] = 1;
dfn[x] = ++ dfc;
fdf[dfn[x]] = x;
siz[x] = 1;
st[x] = ++ stc;
for(int i : g[x]){
if(!vis[i]){
dfs(i, x);
++ stc;
siz[x] += siz[i];
} else if(i != fa && dep[x] > dep[i]){
son[i].push_back(x);
}
}
}
vector<vector<int> > create_map(int N, int M, vector<int> A, vector<int> B) {
vector<vector<int> > ans(2 * N, vector<int>(2 * N, 0));
dfc = stc = 0;
for(int i = 1; i <= N; ++ i){
vector<int> ().swap(g[i]);
vector<int> ().swap(son[i]);
vis[i] = dfn[i] = siz[i] = st[i] = fdf[i] = dep[i] = 0;
}
for(int i = 0; i < M; ++ i){
g[A[i]].push_back(B[i]);
g[B[i]].push_back(A[i]);
}
dfs(1, 0);
for(int i = 1; i <= N; ++ i){
for(int j = 2 * i - 2; j < 2 * N; ++ j){
for(int k = st[fdf[i]]-1; k <= st[fdf[i]] + 2 * siz[fdf[i]] - 3; ++ k){
ans[j][k] = fdf[i];
}
}
}
for(int i = 1; i <= N; ++ i){
for(int j = 0; j < siz[fdf[i]] - 1; ++ j){
ans[2*i][st[fdf[i]]+j*2] = fdf[i];
if(j < son[fdf[i]].size()){
ans[2*i-1][st[fdf[i]]+j*2] = son[fdf[i]][j];
}
}
}
for(int i = 0; i < 2 * N; ++ i){
ans[i][2*N-1] = 1;
}
return ans;
}
4. P13540 [IOI 2025] 羊驼的坎坷之旅(obstacles)
https://www.luogu.com.cn/problem/P13540。
题意:给一个 \(n\times m\) 的矩阵,每行一个权值 \(a\),每列一个权值 \(b\),一个格子可以走当且仅当 \(a_i>b_j\)。强制在线询问 \(L,R,S,T\),表示提取出 \([L,R]\) 内的列,点 \((0,S)\) 是否能四联通走到 \((0,T)\)。
\(n,m,q\leq 2\times 10^5\)。
首先不考虑 \(L,R\) 的限制。
不可行等价于有割。若将矩阵的左、下、右三面都加满一条障碍,那么割形如从 \((0,[S+1,T-1])\) 走到 \((0,[0,S-1])\) 或者 \((0,[T+1,m-1])\),可以证明这样的割一定是分为三段直线:向下、向两侧其中一侧、向上。
证明:首先每一列的障碍集合一定两两包含或者不交,然后画出一条不符合上述条件的折线 \((0,S)\to (0, T)\),可以证明要么存在一条满足上述条件的 \(S\to T\),要么存在若干条满足上述条件的 \(S\to P_1,...,P_k\to T\)。所以只用提取出所有满足上述条件的极小割即可。
发现,如果两个割区间有交,他们都不是极小的,所以极小割只有 \(O(n)\) 组。极小割是好求的:对于 \(b_i\),找到它两侧第一个 \(\geq b_i\) 的位置,然后 check \(i\) 和这两个位置分别能不能构成割即可。
考虑 \(L,R\) 的限制,多了一种情况是 \(L,R\) 与原先不是割的一段构成割,此时维护 \(mxl,mxr\) 表示 \(b_i\) 向下后再向两侧最远能够到达哪两列,然后 rmq 即可。
//P13540
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int a[N], b[N], n, m;
int lm[N], rm[N], st[N], tp;
int mxle[N], mxri[N], mxdn[N];
int apmx[N], apmn[N];
int ST[20][N];
int qmn(int l, int r){
int k = 31 ^ __builtin_clz(r - l + 1);
return min(ST[k][l], ST[k][r-(1<<k)+1]);
}
int mn[N*4], mx[N*4], mmn[N*4], mmx[N*4];
void add(int p, int l, int r, int x, int v, int op){
if(l == r){
if(op == 0){
mn[p] = min(mn[p], v);
} else if(op == 1){
mx[p] = max(mx[p], v);
} else if(op == 2){
mmn[p] = min(mmn[p], v);
} else if(op == 3){
mmx[p] = max(mmx[p], v);
}
} else {
int mid = l + r >> 1;
if(x <= mid){
add(p<<1, l, mid, x, v, op);
} else {
add(p<<1|1, mid+1, r, x, v, op);
}
mn[p] = min(mn[p<<1], mn[p<<1|1]);
mx[p] = max(mx[p<<1], mx[p<<1|1]);
mmn[p] = min(mmn[p<<1], mmn[p<<1|1]);
mmx[p] = max(mmx[p<<1], mmx[p<<1|1]);
}
}
int qry(int p, int l, int r, int ql, int qr, int op){
if(qr < l || r < ql){
return (op & 1) ? 0 : m + 1;
} else if(ql <= l && r <= qr){
if(op == 0){
return mn[p];
} else if(op == 1){
return mx[p];
} else if(op == 2){
return mmn[p];
} else {
return mmx[p];
}
} else {
int mid = l + r >> 1;
if(op & 1){
return max(qry(p<<1, l, mid, ql, qr, op), qry(p<<1|1, mid+1, r, ql, qr, op));
} else {
return min(qry(p<<1, l, mid, ql, qr, op), qry(p<<1|1, mid+1, r, ql, qr, op));
}
}
}
void initialize(vector<int> T, vector<int> H){
n = T.size(), m = H.size();
apmn[0] = 2e9;
for(int i = 1; i <= n; ++ i){
a[i] = T[i-1];
apmx[i] = max(a[i], apmx[i-1]);
apmn[i] = min(a[i], apmn[i-1]);
}
a[++n] = 0;
b[1] = b[m+2] = 1e9;
for(int i = 2; i <= m+1; ++ i){
b[i] = H[i-2];
}
m += 2;
for(int i = 1; i <= m; ++ i){
mn[i] = mn[i+m] = mn[i+m+m] = mn[i+m+m+m] = m + 1;
mmn[i] = mmn[i+m] = mmn[i+m+m] = mmn[i+m+m+m] = m + 1;
ST[0][i] = b[i];
int L = 0, R = n;
while(L < R){
int mid = L + R + 1 >> 1;
if(apmx[mid] <= b[i]){
L = mid;
} else {
R = mid - 1;
}
}
mxdn[i] = L;
}
for(int i = 1; i < 20; ++ i){
for(int j = 1; j + (1 << i) - 1 <= m; ++ j){
ST[i][j] = min(ST[i-1][j], ST[i-1][j+(1<<i-1)]);
}
}
tp = 0;
for(int i = 1; i <= m; ++ i){
int mn = apmn[mxdn[i]];
int L = 1, R = i;
while(L < R){
int mid = L + R >> 1;
if(qmn(mid, i) >= mn){
R = mid;
} else {
L = mid + 1;
}
}
mxle[i] = L;
L = i, R = m;
while(L < R){
int mid = L + R + 1 >> 1;
if(qmn(i, mid) >= mn){
L = mid;
} else {
R = mid - 1;
}
}
mxri[i] = L;
add(1, 1, m, i, mxle[i], 2);
add(1, 1, m, i, mxri[i], 3);
while(tp && b[st[tp]] < b[i]){
-- tp;
}
if(tp){
int mn = min(mxdn[i], mxdn[st[tp]]);
if(apmn[mn] <= qmn(st[tp], i)){
add(1, 1, m, i, st[tp], 0);
add(1, 1, m, st[tp], i, 1);
}
}
st[++tp] = i;
}
tp = 0;
for(int i = m; i >= 1; -- i){
while(tp && b[st[tp]] < b[i]){
-- tp;
}
if(tp){
int mn = min(mxdn[i], mxdn[st[tp]]);
if(apmn[mn] <= qmn(i, st[tp])){
add(1, 1, m, i, st[tp], 1);
add(1, 1, m, st[tp], i, 0);
}
}
st[++tp] = i;
}
}
bool can_reach(int L, int R, int S, int D){
if(S == D){
return 1;
}
if(S > D){
swap(S, D);
}
L += 1;
R += 3;
S += 2;
D += 2;
if(qry(1, 1, m, S+1, D-1, 0) < S){
return 0;
}
if(qry(1, 1, m, S+1, D-1, 1) > D){
return 0;
}
if(qry(1, 1, m, S+1, D-1, 2) <= L + 1){
return 0;
}
if(qry(1, 1, m, S+1, D-1, 3) >= R - 1){
return 0;
}
return 1;
}
5. P13606 [NWRRC 2022] IQ Game
https://www.luogu.com.cn/problem/P13606。
题意:一个 \(len\) 个点的环上有 \(n\) 个人,一个关键人,每个人都在一个点上。每次随机选一个环上的点,然后从这个点开始顺时针经过的第一个人离场,关键人离场后结束。求期望离场人数。
\(len\leq 10^9,n\leq 200\)。
\(O(n^4)\) 的歪解。
首先在关键人处断环为链,这样关键人就在序列的最后一个位置。
由期望线性性,期望离场人数等于枚举每个非关键人,这个人先于关键人离场的概率之和。
假设目前枚举到的人为 \(i\),然后枚举 \(i\) 离场时,在他左边操作的次数 \(x\);右边操作的次数 \(y\),如果能够求到 \(F\):左边操作 \(x\) 次,第 \(x\) 次使得人 \(i\) 离场;\(G\):右边操作 \(y\) 次,最后一个人不离场,那么答案是好算的,即:单个概率乘以考虑操作顺序后本质相同的方案数:
最后考虑 \(F,G\) 怎么求了,容易发现从后往前求是比较简单的:对于 \([i,n]\) 上的操作,只需满足 \([j,n]\) 操作数 \(\leq n-j\) 即可,那么记录 \(g_{i,j}\) 表示 \([i,n]\) 操作 \(j\) 次,最后一个人不离场的方案数:
\(G\) 取 \(g_{i+1,y}\) 即可。
\(F\) 同理,只不过要对于每个 \(i\) 从右往左算一次,取 \(f_{1,x-1}\times \dfrac{a_i}{len} - f_{1,x}\)。
复杂度瓶颈在于每次转移 \(F\)。可能可以通过转置之类的东西优化到三次方,但是我不会。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 998244353;
const int N = 210;
int len, n, S, a[N];
ll C[N][N];
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
ll f[N][N], g[N][N];
int main(){
scanf("%d%d%d", &len, &n, &S);
C[0][0] = 1;
for(int i = 1; i <= n; ++ i){
C[i][0] = 1;
for(int j = 1; j <= i; ++ j){
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
}
scanf("%d", &a[i]);
if(a[i] <= S){
a[i] += len - S;
} else {
a[i] -= S;
}
}
sort(a + 1, a + n + 1);
ll in = qp(len, P - 2);
g[n][0] = 1;
for(int i = n-1; i >= 1; -- i){
for(int j = 0; j <= n - i; ++ j){
ll val = 1;
for(int k = j; k >= 0; -- k){
g[i][j] = (g[i][j] + g[i+1][k] * val % P * C[j][k]) % P;
val = val * (a[i] - a[i-1]) % P * in % P;
}
}
}
ll ans = 0;
for(int i = 1; i < n; ++ i){
memset(f, 0, sizeof(f));
f[i][0] = 1;
for(int p = i-1; p >= 1; -- p){
for(int j = 0; j <= i - p; ++ j){
ll val = 1;
for(int k = j; k >= 0; -- k){
f[p][j] = (f[p][j] + f[p+1][k] * val % P * C[j][k]) % P;
val = val * (a[p] - a[p-1]) % P * in % P;
}
}
}
for(int x = 1; x <= i; ++ x){
for(int y = 0; y < n - i; ++ y){
ll F = (f[1][x-1] * a[i] % P * in % P - f[1][x] + P) % P;
ll G = g[i+1][y];
ans = (ans + C[x+y-1][y] % P * F % P * G) % P;
}
}
}
printf("%lld\n", (ans + 1) % P);
return 0;
}
6. P12573 [UOI 2023] An Array and XOR
https://www.luogu.com.cn/problem/P12573。
题意:给定一个整数 \(m\),一个长度为 \(n\) 的非负整数数组 \(a\),以及 \(q\) 个形如 \(l_i\), \(r_i\) 的查询。数组 \(a\) 的所有元素都小于 \(2^m\)。定义函数 \(f_i(x) = \min_{j \in [l_i, r_i]} (a_j \oplus x)\),其中 \(\oplus\) 表示按位异或运算。对于每个查询,你需要找到 \(\max_{x \in \{0, 1, \ldots, 2^m-1\}} f_i(x)\) 的值。
\(n\leq 10^5,m\leq 50,q\leq 5\times 10^5\)。
容易转化为:建 trie 树,\(a_i\) 的最大 \(x\) 是:代表 \(a_i\) 的那条链上,没有兄弟的点的权值和。
直接莫队可以做到 \(O(nm\sqrt q)\)。
优化是:对于每一位 \(j\),找到 \(le_i,ri_i\) 表示 \(i\) 两侧第一个在 \(j\) 位处分开的数,转化为二维数点问题。
7. P11714 [清华集训 2014] 主旋律
https://www.luogu.com.cn/problem/P11714。
题意:给你一张无重边自环的有向图 \(G(V,E)\),求 \(E\) 的子集 \(E'\) 数量,使得 \(G(V,E')\) 是强连通的。
\(n\leq 15\)。
设 \(E_{S,T}\) 表示 \(\sum_{i\in S,j\in T}[(i,j)\in E]\)。
首先考虑 DAG 计数:
设 \(f_S\) 表示 \(S\) 内的点构成 DAG 的方案数,转移的时候枚举 \(0\) 入度点集合 \(T\),那么 \(T\to S/T\) 的边可以任意连,\(T\) 内没有边,\(S\) 也为一个 DAG。但是不能保证所有 \(S/T\) 的点都有入度,所以需要容斥。
具体的,设 \(F_{T,S}\) 表示 \(S\) 内恰好 \(T\) 是 \(0\) 入度点集合的方案数;\(G_{T,S}\) 表示 \(S\) 内选定 \(T\) 是 \(0\) 入度点集合的方案数,则有:
子集反演:
使用 \(F\) 算 \(f\):
这也就是容斥系数是 \((-1)^{|T|+1}\) 的原因。
现在开始强连通图计数。忘记上述的状态定义,重新定义 \(f_S\) 为使得 \(S\) 为强连通图的方案数。
同样的,强连通图缩点后为一个点;否则为一个 \(\geq 2\) 个点的 DAG。枚举 \(0\) 入度 SCC 由 \(S\) 内的点构成。
如果直接定义 \(g_S\) 表示 \(S\) 内点构成若干个 SCC 的方案数的话,容斥系数 \((-1)^{|T|+1}\) 中的 \(|T|\) 代表 SCC 个数就无法表示了,所以我们考虑将容斥系数与 \(g\) 统一定义:\(g_{S}\) 表示 \(S\) 内点构成奇数个 SCC 方案数减去构成偶数个 SCC 方案数。
写出式子:
此时的 \(S/T\) 部分并没有要求是 DAG/SCC,而是任意一张图都可以。
注意到:此处 \(f\) 的式子有问题:如果将 \(g_S\) 带入 \(f_S\) 的式子中,两侧都出现了 \(f_S\)。但是考虑到此时的意义是:入度为 \(0\) 的 SCC 只有一个,且整张图由这一个 SCC 构成,不应该被带入到 \(f_S\) 的式子中。
解决方法是先当作 \(f_S=0\) 计算 \(g_S\),然后计算 \(f_S\),最后计算最终的 \(g_S\)。
最后问题:计算 \(E\)。观察到有用的 \(E_{A,B}\) 要么是 \(A=B\),要么是 \(A\cup B=S,A\cap B=\emptyset\)。
前者递推式:
后者,当计算到 \(S\) 时,递推:
总复杂度 \(O(3^n)\),空间 \(O(2^n)\)。
//P11714
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define ppc __builtin_popcount
const ll P = 1e9 + 7;
int n, m, in[15], out[15];
ll pw[220], E[1<<15], f[1<<15], g[1<<15], EE[1<<15];
int main(){
pw[0] = 1;
for(int i = 1; i < 220; ++ i){
pw[i] = pw[i-1] * 2 % P;
}
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++ i){
int x, y;
scanf("%d%d", &x, &y);
-- x;
-- y;
in[y] |= (1 << x);
out[x] |= (1 << y);
}
for(int S = 1; S < (1 << n); ++ S){
int x = 31 ^ __builtin_clz(S & -S);
E[S] = E[S^(1<<x)] + ppc(in[x]&S) + ppc(out[x]&S);
}
for(int S = 1; S < (1 << n); ++ S){
EE[S] = 0;
for(int T = (S - 1) & S; T; T = (T - 1) & S){
int x = 31 ^ __builtin_clz((S^T) & -(S^T));
EE[T] = EE[T^(1<<x)] + ppc(in[x]&T) - ppc(out[x]&(S^T));
if((S & -S) == (T & -T)){
g[S] = (g[S] - f[T] * g[S^T] % P + P) % P;
}
}
f[S] = pw[E[S]];
for(int T = S; T; T = (T - 1) & S){
f[S] = (f[S] - g[T] * pw[EE[T]] % P * pw[E[S^T]] % P + P) % P;
}
g[S] = (g[S] + f[S]) % P;
}
printf("%lld\n", f[(1<<n)-1]);
return 0;
}
8. P11834 [省选联考 2025] 岁月
https://www.luogu.com.cn/problem/P11834。
题意:给你一张 \(n\) 个点 \(m\) 条双向有向边的图 \(G\),每条有向边有 \(\dfrac 12\) 的概率消失,设得到的图为 \(G'\)。求图 \(G'\) 的最小外向生成树边权和等于 \(G\) 的的概率。
\(n\leq 15\)。特殊性质:边权 \(=1\)。
特殊性质
此时只要有外向生成树存在,则满足题意。
外向生成树存在等价于:
- 存在一个点集 \(C\),\(C\) 内点强连通,定义这个 \(C\) 为此时的根集。
- \(C\) 内点能到达 \(C\) 外所有点。
- \(C\) 外所有点到达不了 \(C\) 内任意一点。
三部分概率是独立的。
第一部分,等价于上一题主旋律,对于每个子集 \(S\) 求出其构成一个 SCC 的概率。
第二部分,考虑 dp。设目前枚举了点集 \(C\),\(dp_S\) 表示 \(C\) 能走到 \(S\) 内所有点的概率,容斥有一个集合 \(T\) 到达不了:
起始状态为 \(dp_C\) 为第一部分的答案,终点为 \(dp_U\)。
第三部分,答案就是 \(2^{-E_{U/C,C}}\),表示 \(U/C\to C\) 的边全部不能出现。
复杂度 \(O(4^n)\),瓶颈在于第二部分。
考虑转置原理:第二部分的转移,如果没有这个 \(1\),相当于 \(C\to U\) 的所有路径权值积的和。加上这个 \(1\) 就表示 \(C\to U\),每个中间点都能作为一次起点,所有路径权值积的和。
转置后就是所有 \(U\to P\) 的所有路径权值积的和,其中 \(P\) 能够到达 \(C\) 即可。
也就是一次 dp + 一次超集和。最后答案是每个 \(C\) 的上述三部分乘积加起来。复杂度 \(O(n^3)\)。
正解
kruskal 的结论是:任意一棵 MST,只提取 \(\leq w\) 的边,构成的连通块集合都是相同的。
那么,从小到大考虑每个边权,设目前考虑到边权 \(w\):
- 目前会形成若干个连通块,这个连通块形态只和原图形态有关。
- 设 \(bel_i\) 表示点 \(i\) 所在连通块点集。
- 设 \(sc_S\) 表示考虑边权 \(\leq x-1\) 时,集合 \(S\) 内的所有点所在连通块并集。
- 设 \(to_i\) 表示一端为 \(i\),边权为 \(w\) 的另一端点点集。
- 设 \(eg\_cnt_S\) 表示有多少条边权为 \(w\) 的边两端都在 \(S\) 内。
- 设 \(is_S\) 边权 \(w-1\to w\) 时,集合 \(S\) 所在连通块是否会改变(即这一轮这个集合是否需要计算)。
- 简单的计算:\(E(S,T)=eg\_cnt_{S|T}-eg\_cnt_S-eg\_cnt_T\)。
对于连通块 \(B\),它的内部集合 \(S\) 会以一个概率成为 \(B\) 内的根集,设为 \(ans_B\)(如果 \(B\) 不在一个连通块内,\(ans_B\) 看作未定义)
那么当考虑所有 \(w\) 边,我们需要做的是合并若干个连通块,然后更新 \(ans\) 数组。同样分为三部分:
Part 1. 点集 \(R\) 内部强连通
计算 \(R\) 与每一个连通块 \(B_i\) 的交集 \(R_i\),则相当于 \(R_1,...,R_k\) 这些各自连通块内的根集用连通块之间的 \(w\) 边实现强连通。和主旋律大体相同,区别在于 \(B_i/R_i\to R_j\) 的边也可以看作 \(R_i\to R_j\) 的边,因为 \(R_i\to B_i/R_i\) 必定可行。把 \(B_i\to R_j\) 称作对 \(i\to j\) 有用的边。修改主旋律转移式系数即可。
注意要求 \(sc_{S/T}\cap sc_T=0\),即 \(S/T,T\) 在不同的连通块内。此处的定义是 \(S/T\) 和 \(S\) 分别在不同 SCC 内,之间的所有有用的边都要断开。
此处要求 \(S/T\) 零入度。答案 \(f_R\)
Part 2. \(R\) 能到 \(sc_R\) 内所有点
设 \(dp_S\) 表示 \(R\) 能够走到 \(S\) 这个根集的概率(也就是说 \(R\) 能走到 \(sc_S\) 内所有点)。
这里需要考虑一下全集的概率,实际上是 \(S\) 在每个连通块内的部分都是根集的概率。设为 \(be_S= \prod ans_{S\operatorname{and}B_i}\)。
转移:
终点是所有 \(sc_S=sc_R\) 的 \(S\)。
转置方法是先找到这样的 \(S\) 然后令 \(dp_S=1\),接着倒着做这个 dp,然后每个位置 \(\times be_S\) 再贡献到它的子集作为答案的一部分。
Part 3. 没有边从完全与 \(R\) 不交的连通块里的点连进 \(R\)
对于 \(R\) 计算出这样连通块能连 \(cnt\) 条边,然后 \(\times 2^{-cnt}\) 即可。
最后用 \(dp_R\times 2^{-cnt}\times f_R\) 更新 \(ans_R\)。
最后答案是 \(\sum_{S\neq \emptyset} ans_S\)。
//P11834
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 1e9 + 7;
ll pw[1000], ipw[1000];
ll qp(ll x, ll y){
ll ans = 1;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
const ll i2 = qp(2, P - 2);
int n, m;
struct edge{
int u, v, w;
} eg[500];
bool cmp(edge x, edge y){
return x.w < y.w;
}
int fa[15];
int gf(int x){
return x == fa[x] ? x : fa[x] = gf(fa[x]);
}
ll ans[1<<15];//集合 S 作为根集概率
int sc[1<<15], new_sc[1<<15];//集合 S 所在连通块并集(本轮/下一轮)
int bel[15];//点 i 所在连通块集合
int to[1<<15];//点 i 邻域集合
ll be[1<<15];//集合 S 作为 sc_S 的根集的概率
int eg_cnt[1<<15];//集合 S 内目前需要考虑的边数
bool is[1<<15];//集合 S 目前是否需要考虑
ll f[1<<15], g[1<<15], dp[1<<15];
int E(int S, int T){//S,T 内边数
return eg_cnt[S|T] - eg_cnt[S] - eg_cnt[T];
}
void solve(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= m; ++ i){
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
eg[i] = {u - 1, v - 1, w};
}
sort(eg + 1, eg + m + 1, cmp);
for(int i = 0; i < n; ++ i){
fa[i] = i;
ans[1<<i] = 1;
bel[i] = 1 << i;
}
for(int S = 0; S < (1 << n); ++ S){
new_sc[S] = S;
}
for(int l = 1, r = 1; l <= m; l = r + 1){
r = l;
while(r + 1 <= m && eg[r+1].w == eg[l].w){
++ r;
}
swap(sc, new_sc);
memset(new_sc, 0, sizeof(new_sc));
memset(is, 0, sizeof(is));
memset(to, 0, sizeof(to));
for(int i = l; i <= r; ++ i){
if(gf(eg[i].u) != gf(eg[i].v)){
bel[gf(eg[i].v)] |= bel[gf(eg[i].u)];
fa[gf(eg[i].u)] = gf(eg[i].v);
}
to[eg[i].u] |= 1 << eg[i].v;
to[eg[i].v] |= 1 << eg[i].u;
}
for(int i = 0; i < n; ++ i){
bel[i] = bel[gf(i)];
}
be[0] = 1;
eg_cnt[0] = 0;
for(int S = 1; S < (1 << n); ++ S){
int x = 31 ^ __builtin_clz(S & -S);
be[S] = be[S^(S&sc[1<<x])] * ans[S&sc[1<<x]] % P;
new_sc[S] = new_sc[S^(S&bel[x])] | bel[x];
eg_cnt[S] = 0;
for(int i = l; i <= r; ++ i){
if(((S >> eg[i].u) & 1) && ((S >> eg[i].v) & 1)){
++ eg_cnt[S];
}
}
}
for(int i = 0; i < n; ++ i){
if(gf(i) == i && sc[1<<i] != new_sc[1<<i]){
for(int S = bel[i]; S; S = (S - 1) & bel[i]){
is[S] = 1;
}
}
}
//Part 1. 主旋律
memset(f, 0, sizeof(f));
memset(g, 0, sizeof(g));
for(int S = 1; S < (1 << n); ++ S){
if(!is[S]){
continue;
}
for(int T = (S - 1) & S; T; T = (T - 1) & S){
if((sc[S^T] & sc[T]) == 0 && (S & -S) == (T & -T)){
g[S] = (g[S] - f[T] * g[S^T] % P * ipw[E(sc[S^T],T)+E(sc[T],S^T)] % P + P) % P;
}
}
f[S] = 1;
for(int T = S; T; T = (T - 1) & S){
if((sc[S^T] & sc[T]) == 0){
f[S] = (f[S] - g[T] * ipw[E(sc[S^T],T)] % P + P) % P;
}
}
g[S] = (g[S] + f[S]) % P;
}
//Part 2. Others
for(int S = 1; S < (1 << n); ++ S){
if(!is[S]){
continue;
}
int cnt = 0;
for(int i = 0; i < n; ++ i){
if(new_sc[1<<i] == new_sc[S] && (sc[1<<i] & sc[S]) == 0){
cnt += __builtin_popcount(S & to[i]);
}
}
f[S] = f[S] * ipw[cnt] % P;
}
//Part 3. 转置
memset(dp, 0, sizeof(dp));
for(int S = 1; S < (1 << n); ++ S){
if(!is[S]){
continue;
}
int flg = 1;
for(int i = 0; i < n; ++ i){
if(new_sc[1<<i] == new_sc[S] && (sc[1<<i] & sc[S]) == 0){
flg = 0;
break;
}
}
dp[S] = flg;
}
for(int S = (1 << n) - 1; S; -- S){
if(!is[S]){
continue;
}
for(int T = (S - 1) & S; T; T = (T - 1) & S){
if((sc[S^T] & sc[T]) == 0){
dp[S^T] = (dp[S^T] + dp[S] * (P - ipw[E(sc[S^T], T)]) % P * be[T]) % P;
}
}
}
for(ll S = 1; S < (1 << n); ++ S){
if(dp[S]){
dp[S] = dp[S] * be[S] % P;
for(ll T = (S - 1) & S; T; T = (T - 1) & S){
if((sc[S^T] & sc[T]) == 0){
dp[T] = (dp[T] + dp[S]) % P;
}
}
}
}
for(int S = 1; S < (1 << n); ++ S){
if(!is[S]){
continue;
}
ans[S] = f[S] * dp[S] % P;
}
}
ll res = 0;
for(int S = 1; S < (1 << n); ++ S){
res = (res + ans[S]) % P;
}
printf("%lld\n", res);
return;
}
int main(){
pw[0] = ipw[0] = 1;
for(int i = 1; i < 1000; ++ i){
pw[i] = pw[i-1] * 2 % P;
ipw[i] = ipw[i-1] * i2 % P;
}
int c, T;
scanf("%d%d", &c, &T);
while(T--){
solve();
}
return 0;
}
9. P11118 [ROI 2024] 无人机比赛 (Day 2)
https://www.luogu.com.cn/problem/P11118。
题意:有一条赛道,每隔若干格有一个存档点,坐标分别是 \(s_1,s_2,...,s_m\)。\(n\) 个无人机进行比赛,第 \(i\) 个无人机飞 \(1\) 单位距离的时间是 \(t_i\)。每当一个无人机飞到一个存档点,它留在存档点,其他所有还未完赛的无人机全部返回到它最后一次存档的存档点(如果有多个无人机同时到达存档点,看作编号最小的到达其他的没有到达),定义一次返回存档点的操作为传送。一个无人机飞到存档点 \(m\) 即视为完赛,不参与后续的过程。对于每个 \(i\),求 \([1,i]\) 内的无人机进行比赛,整个过程中所有无人机在比赛结束之前一共将进行多少次传送。
\(n,m,s_m\leq 150000,t\leq 10^9\)。
设无人机 \(i\) 从存档点 \(j-1\) 飞到存档点 \(j\) 的时间为 \(w_{i,j}=t_i(s_j-s_{j-1})\),则操作序列相当于将这些 \(w\) 进行归并,每次选开头的最小的 \(w\) 放入队列中。
观察到如果 \(w_{i,j}\geq w_{i,j+1}\),则 \(w_{i,j+1}\) 一定紧跟在 \(w_{i,j}\) 之后,则直接合并即可。然后对于一个 \(i\),每一段开头的 \(w\) 值严格递增,于是每个 \(i\) 的 \(w\) 构成 \(\sqrt{s_m}\) 段。又发现 \(w\) 的大小关系和 \(i\) 无关,所以每一个 \(i\) 的分段都是相同的。
把操作序列每一步分别属于哪个无人机写出来,构成一个长度为 \(nm\),每个数出现 \(m\) 次的数列。计算这个数列对应的传送次数,我们可以发现等于 \(\sum_{i=1}^{nm}i[\forall j>i,col_i\neq col_j]-nm\),即找到每个无人机最后一次操作的下标累加起来再减去 \(nm\) 即可。
所以,对于那些不是最后一段的 \(w\),他们之间的大小关系是没有用的,只用关心 \(>\) 他们的最小 \(w_{*,m}\) 是什么即可。所以维护 \(n\) 个集合,每个集合表示小于某个 \(w_{*,m}\) 的操作段有哪些,然后遍历 \(i\in[1,n]\),将 \(w_{i,*}\) 插入对应集合中,然后激活 \(w_{i,m}\) 对应的集合(即这个集合的下标要计入答案),我们得到了一个正确的,看起来很好优化的做法。
问题 1:如何找到一个 \(w\) 对应的集合?对于段 \(p\),我们将 \(w_{i,p}\) 排序后双指针即可。复杂度 \(O(n\sqrt{s_m})\)。
问题 2:答案如何快速计算?我们会有 \(O(n\sqrt{s_m})\) 次集合大小的单点加,然后 \(n\) 次激活,每次激活查询一个前缀,使用 \(O(1)-O(\sqrt n)\) 分块。然后每次单点加的贡献是它后面的激活集合数,使用 \(O(\sqrt n)-O(1)\) 维护后缀激活集合数。
总复杂度 \(O(n\sqrt{s_m}+n\sqrt n)\)。
//P11118
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 150010, B = 550;
int n, m, t[N], s[N];
int mm, S[N], len[N];
pair<ll, int> bar[N];
int pos[N], hav[N];
int to[551][N];
int ind[N], le[N];
struct SQRT{
ll val[N], blv[N];
void add(int x, ll v){
val[x] += v;
blv[ind[x]] += v;
}
ll ask(int x){
ll ans = 0;
for(int i = 1; i < ind[x]; ++ i){
ans += blv[i];
}
for(int i = le[ind[x]]; i <= x; ++ i){
ans += val[i];
}
return ans;
}
} T1;
struct SQRT2{
int val[N], blv[N];
void add(int x, int v){
for(int i = 1; i < ind[x]; ++ i){
blv[i] += v;
}
for(int i = le[ind[x]]; i <= x; ++ i){
val[i] += v;
}
}
int ask(int x){
return val[x] + blv[ind[x]];
}
} T2;
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++ i){
scanf("%d", &t[i]);
ind[i] = (i - 1) / B + 1;
if(ind[i] != ind[i-1]){
le[ind[i]] = i;
}
}
for(int i = 1; i <= m; ++ i){
scanf("%d", &s[i]);
}
for(int i = m; i >= 1; -- i){
s[i] = s[i] - s[i-1];
}
for(int i = 1; i <= m; ++ i){
if(s[i] > S[mm]){
S[++mm] = s[i];
}
++ len[mm];
}
swap(mm, m);
for(int i = 1; i <= n; ++ i){
bar[i] = make_pair(1ll * t[i] * S[m], i);
}
sort(bar + 1, bar + n + 1);
for(int i = 1; i <= n; ++ i){
pos[bar[i].second] = i;
}
for(int i = 1; i < m; ++ i){
int pp = 1;
for(int j = 1; j <= n; ++ j){
ll val = bar[j].first / S[m];
int id = bar[j].second;
while(bar[pp] <= make_pair(1ll * val * S[i], id)){
++ pp;
}
to[i][id] = pp;
}
}
ll ans = 0;
for(int i = 1; i <= n; ++ i){
for(int j = 1; j < m; ++ j){
T1.add(to[j][i], len[j]);
ans += 1ll * len[j] * T2.ask(to[j][i]);
}
ans += 1ll * len[m] * T2.ask(pos[i]);
T1.add(pos[i], len[m]);
ans += T1.ask(pos[i]);
T2.add(pos[i], 1);
printf("%lld\n", ans - 1ll * mm * i);
}
return 0;
}