2024.3 武汉集训做题笔记
2024.3 武汉集训做题笔记
数据结构
CF983E NN country
没啥意思,每个点一定跳到能往上走的最上的点,先跳到还有一步能到 lca 时跳到的最远地方,设 \(x,y\) 分别跳到了 \(u,v\),则最后判断是否有一条路径同时覆盖 \(u,v\),如果有则还需一步,否则要两步
这就是二维数点,扫描线,向上跳由于每个点每次跳一步的方案固定,可以倍增,时间复杂度 \(O(n\log n)\)
code
struct qry {int l, r, id;}; vector<qry> ask[N];
struct BIT
{
int sum[N];
void upd(int x, int val) {for(; x <= n; x += x & (-x)) sum[x] += val;}
int query(int x) {int res = 0; for(; x; x -= x & (-x)) res += sum[x]; return res;}
int ask(int l, int r) {return query(r) - query(l - 1);}
}bit;
void dfs(int x)
{
dep[x] = dep[fa[x]] + 1, dfn[x] = ++cnt, rmq[0][cnt] = fa[x];
for(int y : edge[x]) dfs(y);
mxdfn[x] = cnt;
}0
int getlca(int x, int y)
{
if(dfn[x] > dfn[y]) swap(x, y);
int l = dfn[x] + 1, r = dfn[y], v = lg[r - l + 1];
return dfn[rmq[v][l]] < dfn[rmq[v][r - (1 << v) + 1]] ? rmq[v][l] : rmq[v][r - (1 << v) + 1];
}
void Dfs(int x)
{
for(int y : edge[x])
{
Dfs(y);
if(dep[f[0][y]] < dep[x] && dep[f[0][y]] < dep[f[0][x]]) f[0][x] = f[0][y];
}
}
pii jump(int x, int y)
{
int res = 0;
for(int i = 17; i >= 0; --i)
if(f[i][x] && dep[f[i][x]] > dep[y]) x = f[i][x], res += 1 << i;
return {x, res};
}
int main()
{
#ifdef Kelly
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
freopen("err.txt", "w", stderr);
#endif
read(n), lg[0] = -1;
for(int i = 2; i <= n; ++i) read(fa[i]), edge[fa[i]].pb(i);
for(int i = 1; i <= n; ++i) lg[i] = lg[i >> 1] + 1;
dfs(1), dep[0] = N;
for(int j = 1; j <= 17; ++j)
for(int i = 1; i + (1 << j) - 1 <= n; ++i)
{
int ln = rmq[j - 1][i], rn = rmq[j - 1][i + (1 << (j - 1))];
rmq[j][i] = dfn[ln] < dfn[rn] ? ln : rn;
}
read(m);
for(int i = 1; i <= m; ++i)
{
read(u, v);
if(dfn[u] > dfn[v]) swap(u, v);
int lca = getlca(u, v);
if(dep[lca] < dep[u] && dep[f[0][u]] > dep[lca]) f[0][u] = lca;
if(dep[lca] < dep[v] && dep[f[0][v]] > dep[lca]) f[0][v] = lca;
upd[dfn[u]].pb(dfn[v]);
}
Dfs(1);
for(int j = 1; j <= 17; ++j)
for(int i = 1; i <= n; ++i) f[j][i] = f[j - 1][f[j - 1][i]];
read(q);
for(int i = 1; i <= q; ++i)
{
read(u, v);
int lca = getlca(u, v);
if(dfn[u] < dfn[v]) swap(u, v);
pii x = jump(u, lca), y = jump(v, lca);
if(v == lca) ans[i] = dep[f[0][x.fi]] <= dep[v] ? x.se + y.se + 1 : -1;
else
{
ans[i] += x.se + y.se + 2;
if(dep[f[0][x.fi]] > dep[lca] || dep[f[0][y.fi]] > dep[lca]) ans[i] = -1;
else ask[dfn[y.fi] - 1].pb({dfn[x.fi], mxdfn[x.fi], -i}), ask[mxdfn[y.fi]].pb({dfn[x.fi], mxdfn[x.fi], i});
}
}
for(int i = 1; i <= n; ++i)
{
for(int x : upd[i]) bit.upd(x, 1);
for(qry x : ask[i]) x.id > 0 ? tot[x.id] += bit.ask(x.l, x.r) : tot[-x.id] -= bit.ask(x.l, x.r);
}
for(int i = 1; i <= q; ++i) print(ans[i] - (tot[i] > 0 ? 1 : 0)), putchar('\n');
return 0;
}
CF1578B Building Forest Trails
断环为链,发现一个连通块的连边,可以简化为只有相邻的点连边,容易发现不会影响相交情况
我们设一个点 \(x\) 上方覆盖的边数为 \(h_x\)(不包括端点刚好是 \(x\) 的边)
每次连边 \(u,v\),从 \(u,v\) 处不断向中间靠近,设 \(h_u > h_v\),\(u\) 右边第一个 \(h_x < h_u\) 的地方为 \(x\),则 \(u\) 和 \(x\) 连边后一定在同一个连通块,把它们合并
直到当 \(h_u = h_v\) 时,如果 \(u,v\) 中间还有一个 \(h\) 更小的位置,那么它也在这个连通块中,合并,否则直接合并 \(u,v\) 并结束整个过程
合并同时需要维护每个点的 \(h\),使用线段树维护,并查集维护连通块的同时记录每个连通块最靠左的点和最靠右的点,如果两个连通块不交,则它们最靠左的点和最靠右的点有连边,中间点的 \(h\) 增加 \(1\),否则相交部分的 \(h\) 减少 \(1\)
总共只会发生 \(O(n)\) 次合并,复杂度 \(O(n\log n)\)
code
struct DSU
{
int fa[N], lef[N], rit[N];
int find(int x) {return x != fa[x] ? fa[x] = find(fa[x]) : x;}
void init() {for(int i = 1; i <= n; ++i) fa[i] = lef[i] = rit[i] = i;}
void merge(int x, int y)
{
x = find(x), y = find(y);
if(x != y) lef[y] = min(lef[y], lef[x]), rit[y] = max(rit[y], rit[x]), fa[x] = y;
}
}dsu;
struct segtree
{
int mn[N << 2], tag[N << 2];
int ls(int x) {return x << 1;}
int rs(int x) {return x << 1 | 1;}
void pushup(int x) {mn[x] = min(mn[ls(x)], mn[rs(x)]);}
void upd(int x, int val) {mn[x] += val, tag[x] += val;}
void pushdown(int x) {if(tag[x] != 0) upd(ls(x), tag[x]), upd(rs(x), tag[x]), tag[x] = 0;}
void update(int l, int r, int val, int nl, int nr, int p)
{
if(l > r) return;
if(l <= nl && nr <= r) return upd(p, val);
pushdown(p);
int mid = (nl + nr) >> 1;
if(mid >= l) update(l, r, val, nl, mid, ls(p));
if(mid < r) update(l, r, val, mid + 1, nr, rs(p));
pushup(p);
}
int findright(int x, int val, int l, int r, int p) // right of x < val
{
if(mn[p] >= val) return x - 1;
if(l == r) return mn[p] < val ? l : x - 1;
int mid = (l + r) >> 1;
pushdown(p);
int res = x - 1;
if(mid > x && mn[ls(p)] < val) res = findright(x, val, l, mid, ls(p));
if(res <= x) res = findright(x, val, mid + 1, r, rs(p));
return res;
}
int findleft(int x, int val, int l, int r, int p)
{
if(mn[p] >= val) return x + 1;
if(l == r) return mn[p] < val ? l : x + 1;
int mid = (l + r) >> 1;
pushdown(p);
int res = x + 1;
if(mid + 1 < x && mn[rs(p)] < val) res = findleft(x, val, mid + 1, r, rs(p));
if(res >= x) res = findleft(x, val, l, mid, ls(p));
return res;
}
int query(int pos, int l, int r, int p)
{
if(l == r) return mn[p];
pushdown(p);
int mid = (l + r) >> 1;
if(mid >= pos) return query(pos, l, mid, ls(p));
return query(pos, mid + 1, r, rs(p));
}
}tree;
void join(int x, int y)
{
int u = dsu.find(x), v = dsu.find(y);
if(dsu.lef[u] > dsu.lef[v]) swap(x, y), swap(u, v);
if(dsu.rit[u] < dsu.lef[v]) tree.update(dsu.rit[u] + 1, dsu.lef[v] - 1, 1, 1, n, 1);
else tree.update(dsu.lef[v], min(dsu.rit[v], dsu.rit[u]), -1, 1, n, 1);
dsu.merge(u, v);
}
int main()
{
#ifdef Kelly
freopen("in.txt", "r", stdin);
freopen("out.txt", "w", stdout);
freopen("err.txt", "w", stderr);
#endif
read(n, m);
dsu.init();
for(int i = 1; i <= m; ++i)
{
read(op, u, v);
if(u > v) swap(u, v);
if(op == 1)
{
int x = tree.query(u, 1, n, 1), y = tree.query(v, 1, n, 1);
while(dsu.find(u) != dsu.find(v))
if(x > y) join(u, tree.findright(u, x, 1, n, 1)), --x;
else if(x < y) join(tree.findleft(v, y, 1, n, 1), v), --y;
else
{
int pos = tree.findright(u, x, 1, n, 1);
if(pos < v && pos > u) join(u, pos), --x;
else {join(u, v); break;}
}
}
else putchar(dsu.find(u) != dsu.find(v) ? '0' : '1');
// cerr << i << "\n";
}
return 0;
}
CF765F Souvenirs
支配对好题
考虑相邻的三个数 \(a,b,c\),只有 \(|a-c| < |a-b|\) 且 \(|a-c| < |b - c|\) 时 \((a,c)\) 才有用
先考虑右边数比左边大的对,右边比左边小的再反过来做一遍
于是发现 \(c - a < b - a\),\(c < b\),\(b-c > c - a\),得 \(c < \frac{a+b}{2}\),则 \(c-a\) 至多是 \(b-a\) 的一半
两个数之间的差每次都小一半,则产生贡献的对一共只有 \(O(n\log V)\) 对,把它们找出来后,扫描线回答询问即可
时间复杂度 \(O(n\log V\log n)\)
code
inline int update(int pre, int id, int val, int l, int r)
{
int p = ++idx;
ls[p] = ls[pre], rs[p] = rs[pre], sum[p] = min(sum[pre], val);
if(l == r) return p;
int mid = (l + r) >> 1;
if(mid >= id) ls[p] = update(ls[pre], id, val, l, mid);
else rs[p] = update(rs[pre], id, val, mid + 1, r);
return p;
}
inline int query(int nw, int l, int r, int nl, int nr)
{
if(l <= nl && nr <= r) return sum[nw];
int mid = (nl + nr) >> 1, res = inf;
if(mid >= l) res = min(res, query(ls[nw], l, r, nl, mid));
if(mid < r) res = min(res, query(rs[nw], l, r, mid + 1, nr));
return res;
}
inline void workup(int x)
{
nxt = query(root[x + 1], a[x] + 1, inf, 1, inf);
if(nxt > n) return;
V = a[nxt] - a[x] + 1, point[x].pb(nxt);
while(V > 1)
{
bef = nxt;
if(bef >= n) return;
nxt = query(root[bef + 1], a[x] + 1, a[x] + (V >> 1) - 1 + 1, 1, inf);
if(nxt > n) return;
point[x].pb(nxt), V = a[nxt] - a[x] + 1;
}
}
inline void workdown(int x)
{
nxt = query(root[x + 1], 1, a[x] + 1, 1, inf);
if(nxt > n) return;
V = a[x] - a[nxt] + 1, point[x].pb(nxt);
while(V > 1)
{
bef = nxt;
if(bef >= n) return;
nxt = query(root[bef + 1], a[x] - (V >> 1) + 1 + 1, a[x] + 1, 1, inf);
if(nxt > n) return;
point[x].pb(nxt), V = a[x] - a[nxt] + 1;
}
}
inline void add(int x, int val)
{
while(x <= n) tree[x] = min(tree[x], val), x += x & (-x);
}
inline int ask(int x)
{
int res = inf;
while(x) res = min(res, tree[x]), x -= x & (-x);
return res;
}
int main()
{
n = read();
for(reg int i = 1; i <= n; ++i) a[i] = read();
memset(sum, 0x3f, sizeof(sum)), memset(tree, 0x3f, sizeof(tree));
for(reg int i = n; i > 0; --i) root[i] = update(root[i + 1], a[i] + 1, i, 1, inf);
for(reg int i = 1; i < n; ++i) workup(i), workdown(i);
m = read();
for(reg int i = 1; i <= m; ++i)
{
li = read(), ri = read();
vec[li].pb(mp(ri, i));
}
for(reg int i = n - 1; i > 0; --i)
{
for(reg int j : point[i])
add(j, abs(a[j] - a[i]));
for(reg pii j : vec[i])
ans[j.se] = ask(j.fi);
}
for(reg int i = 1; i <= m; ++i) print(ans[i]), putchar('\n');
return 0;
}
CF1209G2 Into Blocks (hard version)
考虑 easy version 的做法,只用求出全局答案,考虑将序列在能断开的位置断开,即没有颜色跨过这个位置,则每个段中的颜色必须相同,全部改成众数即可
现在要支持单点修改,要找出每个段及段中出现次数最多的颜色,很神仙的想法是找出每个颜色最左边和最右边的点 \(l,r\),将 \([l,r-1]\) 加上 \(1\),设此时 \(i\) 处的和为 \(h_i\),则每个和为 \(0\) 的位置能断开,而且根据段的定义一种颜色肯定全部出现在某段内,把它的大小放到 \(l\) 处,每段的贡献则是段长减去段内的最大值
线段树维护这个序列,每次要进行区间加减,由于和最小就是 \(0\),相当于区间在最小值处断开,合并两个区间时,分类讨论它们最小值的大小关系,维护区间内已经包含的段的最大值的和,左端点,右端点处未封闭的段的最大值,整个区间内的最大值,还有 \(h_i\) 的最小值
复杂度 \(O(n\log n)\)
code
struct segtree
{
int sum[N << 2], mn[N << 2], tag[N << 2], lm[N << 2], rm[N << 2], mx[N << 2];
int lson(int x) {return x << 1;}
int rson(int x) {return x << 1 | 1;}
void pushup(int x)
{
mn[x] = min(mn[lson(x)], mn[rson(x)]), mx[x] = max(mx[lson(x)], mx[rson(x)]);
/*if(mn[lson(x)] && mn[rson(x)]) // no 0
{
sum[x] = mx[x], rm[x] = lm[x] = 0;
}
else*/ if(mn[lson(x)] > mn[rson(x)]) // 0 in right
{
lm[x] = max(mx[lson(x)], lm[rson(x)]), rm[x] = rm[rson(x)];
sum[x] = sum[rson(x)];
}
else if(mn[rson(x)] > mn[lson(x)]) // 0 in left
{
lm[x] = lm[lson(x)], rm[x] = max(rm[lson(x)], mx[rson(x)]);
sum[x] = sum[lson(x)];
}
else // both have 0
{
sum[x] = sum[lson(x)] + sum[rson(x)] + max(rm[lson(x)], lm[rson(x)]);
lm[x] = lm[lson(x)], rm[x] = rm[rson(x)];
}
}
void pushdown(int x)
{
if(!tag[x]) return;
tag[lson(x)] += tag[x], tag[rson(x)] += tag[x];
mn[lson(x)] += tag[x], mn[rson(x)] += tag[x];
tag[x] = 0;
}
void upd(int l, int r, int val, int nl, int nr, int p) // [l,r] + val
{
if(l <= nl && nr <= r)
{
mn[p] += val, tag[p] += val;
return;
}
int mid = (nl + nr) >> 1;
pushdown(p);
if(mid >= l) upd(l, r, val, nl, mid, lson(p));
if(mid < r) upd(l, r, val, mid + 1, nr, rson(p));
pushup(p);
}
void modify(int id, int val, int l, int r, int p) // sum[id] + val
{
if(l == r) {lm[p] += val, mx[p] += val; return;}
int mid = (l + r) >> 1;
pushdown(p);
if(mid >= id) modify(id, val, l, mid, lson(p));
else modify(id, val, mid + 1, r, rson(p));
pushup(p);
}
}tree;
void update(int tmpl, int tmpr, int nwl, int nwr)
{
if(nwl != tmpl)
{
if(nwl > tmpl) tree.upd(tmpl, nwl - 1, -1, 1, n, 1);
else tree.upd(nwl, tmpl - 1, 1, 1, n, 1);
}
else if(nwr != tmpr)
{
if(nwr > tmpr) tree.upd(tmpr, nwr - 1, 1, 1, n, 1);
else tree.upd(nwr, tmpr - 1, -1, 1, n, 1);
}
}
int main()
{
read(n, q);
for(int i = 1; i <= n; ++i)
{
read(a[i]);
cpos[a[i]].insert(i);
}
for(int i = 1; i <= n; ++i) ++num[pre[a[i]]];
for(int i = 1; i <= V; ++i)
if(cpos[i].size())
{
if(cpos[i].size() > 1) tree.upd(*cpos[i].begin(), *cpos[i].rbegin() - 1, 1, 1, n, 1);
tree.modify(*cpos[i].begin(), cpos[i].size(), 1, n, 1);
}
print(n - tree.sum[1] - tree.lm[1] - tree.rm[1]), putchar('\n');
for(int i = 1; i <= q; ++i)
{
read(u, v);
int tmpl = *cpos[a[u]].begin(), tmpr = *cpos[a[u]].rbegin(), nwl, nwr;
tree.modify(tmpl, -cpos[a[u]].size(), 1, n, 1), cpos[a[u]].erase(u);
if(!cpos[a[u]].empty())
{
nwl = *cpos[a[u]].begin(), nwr = *cpos[a[u]].rbegin();
update(tmpl, tmpr, nwl, nwr), tree.modify(nwl, cpos[a[u]].size(), 1, n, 1);
}
if(!cpos[v].empty())
{
tmpl = *cpos[v].begin(), tmpr = *cpos[v].rbegin();
tree.modify(tmpl, -cpos[v].size(), 1, n, 1), cpos[v].insert(u);
nwl = *cpos[v].begin(), nwr = *cpos[v].rbegin();
update(tmpl, tmpr, nwl, nwr), tree.modify(nwl, cpos[v].size(), 1, n, 1);
}
else cpos[v].insert(u), tree.modify(u, 1, 1, n, 1);
a[u] = v;
print(n - tree.sum[1] - tree.lm[1] - tree.rm[1]), putchar('\n');
}
return 0;
}
[AGC001F] Wide Swap
首先 \(j-i\ge k\) 的限制太难处理了,就令序列 \(a_{p_i}=i\),操作就变成了每次交换 \(a\) 中相邻的两个数,但要求差值 \(\ge k\),希望最小化这个排列的字典序
交换相邻两数相当于冒泡排序,我们考虑差 \(>k\) 的一对数,它们的相对位置不会改变,剩下数的相对位置可以随意交换
这样有限制的对中,前面的数向后面连边,即要求 DAG 字典序最小的拓扑序
这里要注意,求字典序最小的拓扑序,应该是把图上边反转后,求字典序最大的拓扑序,再将序列翻转
不过本题中由于连边方式特殊,两种求出来都一样
我们用线段树维护每个点的入度,由于入度最小是 \(0\),在线段树上找到最靠后的最小值 \(x\),取出,然后将 \(x+k+1\sim n\) 的入度减去 \(1\)
注意这里可能有数在 \(x\) 所在位置前面,应该是有它到 \(x\) 的边而不是 \(x\) 到它的,但是不要紧,它肯定被先取出了,把取出的数入度设为 \(\infty\) 即可
code
struct BIT
{
int sum[N];
void upd(int x, int val) {for(; x <= n; x += x & (-x)) sum[x] += val;}
int qry(int x) {int res = 0; for(; x; x -= x & (-x)) res += sum[x]; return res;}
int ask(int l, int r) {l = max(l, 1), r = min(r, n); return l > r ? 0 : qry(r) - qry(l - 1);}
}bit;
struct segtree
{
int mn[N << 2], tag[N << 2];
int ls(int x) {return x << 1;}
int rs(int x) {return x << 1 | 1;}
void pushup(int x) {mn[x] = min(mn[ls(x)], mn[rs(x)]);}
void upd(int x, int val) {mn[x] += val, tag[x] += val;}
void pushdown(int x) {if(tag[x] != 0) upd(ls(x), tag[x]), upd(rs(x), tag[x]), tag[x] = 0;}
void build(int l, int r, int p)
{
if(l == r) return void(mn[p] = deg[l]);
int mid = (l + r) >> 1;
build(l, mid, ls(p)), build(mid + 1, r, rs(p));
pushup(p);
}
void update(int l, int r, int val, int nl, int nr, int p)
{
if(l <= nl && nr <= r) return upd(p, val);
int mid = (nl + nr) >> 1;
pushdown(p);
if(mid >= l) update(l, r, val, nl, mid, ls(p));
if(mid < r) update(l, r, val, mid + 1, nr, rs(p));
pushup(p);
}
int find(int l, int r, int p)
{
if(l == r) return mn[p] = inf, l;
int mid = (l + r) >> 1, res = 0;
pushdown(p);
if(mn[rs(p)] <= 0) res = find(mid + 1, r, rs(p));
else res = find(l, mid, ls(p));
pushup(p); return res;
}
}tree;
int main()
{
read(n, k);
for(int i = 1; i <= n; ++i) read(p[i]), a[p[i]] = i;
for(int i = n; i > 0; --i)
{
deg[a[i]] = bit.ask(a[i] - k + 1, a[i] + k - 1);
bit.upd(a[i], 1);
}
tree.build(1, n, 1);
for(int i = 1; i <= n; ++i)
{
int x = tree.find(1, n, 1);
ans[n - i + 1] = x;
tree.update(max(1, x - k + 1), min(n, x + k - 1), -1, 1, n, 1);
}
for(int i = 1; i <= n; ++i) p[ans[i]] = i;
for(int i = 1; i <= n; ++i) print(p[i]), putchar('\n');
return 0;
}
动态规划
[AGC007E] Shik and Travel
行走的限制相当于一次必须走完整棵子树才能出去
显然可以二分答案,考虑最朴素的 DP ,设 \(f(x,a,b)\) 表示当进入 \(x\) 到叶子的路径长为 \(a\),从叶子出去的路径长为 \(b\) 时是否可行
发现当 \(a' > a\),\(b' > b\) 时 \((a',b')\) 完全没用了,即 \(a\) 升序排序时 \(b\) 降序
但复杂度看起来还是爆炸
合并两棵子树,先考虑走左子树再走右子树的情况,\(a\) 已经固定了,找到 \(b+a' < lim\) 的最大的 \(a'\),此时它对应的 \(b'\) 最小,即作为新的 \(b\),那么发现有用的 \((a,b)\) 对其实只有 \(\min siz\) 对,复杂度分析就同启发式合并,是 \(O(n\log n)\) 的
合并时由于 \(a,b\) 均有单调性,因此可以双指针加归并排序,注意去重,总复杂度 \(O(n\log n\log V)\)
code
vector<pll> merge(vector<pll> &x, vector<pll> &y, ll u, ll v, int op)
{
vector<pll> z;
if(op) swap(x, y), swap(u, v);
for(int i = 0, j = 0, lst = 0; i < x.size(); lst = j, ++i)
{
while(j < y.size() && y[j].fi + x[i].se + u + v <= lim) ++j;
if(j > lst) z.pb({x[i].fi + u, y[j - 1].se + v});
}
return z;
}
void gb(vector<pll> &x, vector<pll> &y)
{
vector<pll> z;
for(int i = 0, j = 0; i < x.size() || j < y.size(); )
if(i < x.size() && (j >= y.size() || x[i] < y[j])) z.pb(x[i++]);
else z.pb(y[j++]);
x.clear();
for(pll i : z)
if(x.empty() || i.fi > x.back().fi) x.pb(i);
}
void dfs(int x)
{
if(!num[x]) return void(f[x].pb({0, 0}));
dfs(son[x][0]), dfs(son[x][1]);
f[x] = merge(f[son[x][0]], f[son[x][1]], a[x][0], a[x][1], 0);
vector<pll> tmp = merge(f[son[x][0]], f[son[x][1]], a[x][0], a[x][1], 1);
gb(f[x], tmp);
}
int check(ll x)
{
lim = x;
for(int i = 1; i <= n; ++i) f[i].clear();
dfs(1);
return f[1].empty() ? 2 : 1;
}
int main()
{
read(n);
for(int i = 2; i <= n; ++i)
{
read(u, v);
son[u][num[u]] = i, a[u][num[u]++] = v;
}
ll l = 0, r = inf;
while(l < r)
{
ll mid = (l + r) >> 1;
if(check(mid) == 1) r = mid;
else l = mid + 1;
}
cout << l;
return 0;
}
CF379G New Year Cactus
点仙人掌上背包,一条边上不能有不同物品,注意是点仙人掌,要建圆方树
在方点处直接递归进入子节点,在圆点时,如果它子节点中有方点,说明这是一个环,把这个环对应的节点全部找出来,合并背包,注意断环为链的地方首尾相接也要不同,因此枚举开头,只统计链结尾合法的方案
复杂度依然是树形背包,\(O(n^2)\)
code
void dfs(int x, int fa)
{
dfn[x] = low[x] = ++cnt, stk[++top] = x;
for(int y : edge[x])
if(!dfn[y])
{
dfs(y, x), low[x] = min(low[x], low[y]);
if(low[y] >= dfn[x])
{
++idx, col[x] = idx, cut[x] = 1;
while(top && stk[top + 1] != y) col[stk[top]] = idx, cir[idx].pb(stk[top--]);
cir[idx].pb(x), reverse(cir[idx].begin(), cir[idx].end());
}
}
else if(dfn[y] < dfn[x] && y != fa) low[x] = min(low[x], dfn[y]);
}
void merge(int x, int y)
{
for(int i = 0; i <= siz[x] + siz[y]; ++i) tmp[i][0] = tmp[i][1] = tmp[i][2] = -inf;
for(int i = 0; i <= siz[x]; ++i)
for(int j = 0; j <= siz[y]; ++j)
{
tmp[i + j][0] = max(tmp[i + j][0], f[x][i][0] + max(f[y][j][0], f[y][j][2]));
tmp[i + j][1] = max(tmp[i + j][1], f[x][i][1] + max(f[y][j][1], f[y][j][2]));
tmp[i + j][2] = max(tmp[i + j][2], f[x][i][2] + max({f[y][j][0], f[y][j][1], f[y][j][2]}));
}
for(int i = 0; i <= siz[x] + siz[y]; ++i) f[x][i][0] = tmp[i][0], f[x][i][1] = tmp[i][1], f[x][i][2] = tmp[i][2];
}
void work(int u)
{
int nw = cir[u].back();
for(int x : cir[u]) memcpy(g[x], f[x], sizeof(g[x]));
for(int i = 0; i <= siz[nw]; ++i)
for(int j = 0; j < 3; ++j) f[nw][i][j] = -inf;
for(int i = 0; i <= siz[u] + siz[cir[u][0]]; ++i)
for(int j = 0; j < 3; ++j) mx[i][j] = -inf;
for(int i = 0; i < 3; ++i)
{
for(int j = 0; j <= siz[nw]; ++j) f[nw][j][i] = g[nw][j][i];
for(int j = (int)cir[u].size() - 2; j >= 0; --j)
merge(cir[u][j], cir[u][j + 1]), siz[cir[u][j]] += siz[cir[u][j + 1]];
for(int j = 0; j <= siz[cir[u][0]]; ++j)
for(int k = 0; k < 3; ++k)
if(i == k || i == 2 || k == 2) mx[j][k] = max(mx[j][k], f[cir[u][0]][j][k]);
for(int j = 0; j + 1 < cir[u].size(); ++j) siz[cir[u][j]] -= siz[cir[u][j + 1]];
for(int x : cir[u])
for(int j = 0; j <= siz[x]; ++j)
for(int k = 0; k < 3; ++k) f[x][j][k] = g[x][j][k];
for(int j = 0; j <= siz[nw]; ++j)
for(int k = 0; k < 3; ++k) f[nw][j][k] = -inf;
}
for(int i = 0; i <= siz[cir[u][0]] + siz[u]; ++i)
for(int j = 0; j < 3; ++j) f[cir[u][0]][i][j] = mx[i][j];
}
void Dfs(int v, int fa)
{
if(v <= n) siz[v] = 1, f[v][1][0] = f[v][0][2] = 0, f[v][0][1] = 1;
else
{
for(int x : tree[v])
{
if(x == fa) continue;
Dfs(x, v), siz[v] += siz[x];
}
return;
}
for(int u : tree[v])
if(u != fa)
{
Dfs(u, v);
work(u), siz[v] += siz[u];
}
}
int main()
{
read(n, m), root = 1;
for(int i = 1; i <= m; ++i) read(u, v), edge[u].pb(v), edge[v].pb(u);
idx = n, dfs(root, 0);
for(int i = n + 1; i <= idx; ++i)
for(int j : cir[i]) tree[i].pb(j), tree[j].pb(i);
for(int i = 1; i <= n; ++i)
for(int j = 0; j <= n; ++j)
for(int k = 0; k < 3; ++k) f[i][j][k] = -inf;
Dfs(root, 0);f
for(int i = 0; i <= n; ++i) print(max({f[root][i][0], f[root][i][1], f[root][i][2]})), putchar(' ');
return 0;
}
[AGC056B] Range Argmax
神秘 Ad-hoc 题
直接对 \(\{x_i\}\) 计数很困难,考虑把它唯一对应上一个排列,对排列计数就比较好做了
固定了 \(x\) 序列,从大到小加入数,把最大值放在能放的最靠左的地方,区间内只考虑完全包含在它内的询问区间,因为若不是则它的最大值肯定在这个区间外,已经确定了
枚举这个位置为 \(k\),这时对于它右侧的区间没有限制,但对于左侧的来说,如果没有任何一个区间 \([l_i,r_i]\) 完全在当前区间内且同时跨过左侧区间的最大值和 \(k\),那么 \(k\) 完全可以放在左侧区间最大值的地方,而不会影响 \(x\) 序列,即最大值还能往左放,就不合法了
于是设 \(f(l,r,k)\) 表示区间 \([l,r]\) 内的最大值在 \(k\) 时的方案数,预处理出 \([l,r]\) 内跨过 \(k\) 的询问区间中最小的左端点 \(x\),则 \([l,k-1]\) 内最大值在 \([x,k-1]\) 内,后缀和优化转移即可,时间复杂度 \(O(n^3)\)
code
int main()
{
read(n, m);
for(int i = 0; i <= n + 1; ++i)
for(int j = 0; j <= n + 1; ++j)
for(int k = 0; k <= n + 1; ++k) ml[i][j][k] = n + 1;
for(int i = 1; i <= m; ++i)
{
read(nl, nr), ++a[nl][nr];
for(int j = nl; j <= nr; ++j) ml[j][nl][nr] = min(ml[j][nl][nr], nl);
}
for(int i = 1; i <= n; ++i)
for(int j = i; j <= n; ++j)
for(int k = i; k > 0; --k) ml[i][k][j] = min({ml[i][k][j], ml[i][k + 1][j], ml[i][k][j - 1]});
for(int i = 1; i <= n; ++i) f[i][i][i] = g[i][i][i] = 1;
for(int len = 2; len <= n; ++len)
for(int l = 1; l + len - 1 <= n; ++l)
{
int r = l + len - 1;
for(int k = l; k <= r; ++k)
f[l][r][k] = (l < k ? g[l][k - 1][ml[k][l][r]] : 1ll) * (k < r ? g[k + 1][r][k + 1] : 1ll) % mod;
for(int k = r; k >= l; --k) g[l][r][k] = add(g[l][r][k + 1], f[l][r][k]);
}
cout << g[1][n][1];
return 0;
}
[ABC221H] Count Multiset
怎么还是想不到划分数的 DP 方式呢
考虑问题是将 \(n\) 划分为 \(i\) 个数,且划分出的每个数个数不超过 \(m\)
设 \(f(i,j)\) 表示当前划分出了 \(i\) 个数,和为 \(j\) 的方案数,转移可以是整体 \(+1\) 或是加上一个 \(1\),但问题是必须时刻保证 \(1\) 的个数不超过 \(m\)
因此设 \(g(i,j)\) 表示当前划分出了 \(i\) 个数,和为 \(j\) 且没有 \(1\) 的方案数,转移变成枚举放的 \(1\) 的个数,\(f(i,j)\gets g(i-k,j-k)(k\le m)\),\(g(i,j)\gets f(i,j-i)\)
用前缀和优化转移,复杂度 \(O(n^2)\)
code
int main()
{
cin >> n >> m;
f[0][0] = g[0][0] = s[0][0] = 1;
for(int i = 1; i <= n; ++i)
for(int j = i; j <= n; ++j)
{
Add(g[i][j], f[i][j - i]);
s[j - i][i] = add(s[j - i][i - 1], g[i][j]);
Add(f[i][j], add(s[j - i][i], (i >= m + 1 ? mod - s[j - i][i - m - 1] : 0)));
}
for(int i = 1; i <= n; ++i) cout << f[i][n] << "\n";
return 0;
}
P5391 [Cnoi2019] 青染之心
真没啥意思,操作显然可以建出树,暴力背包即可,但是空间开不下
于是重链剖分,只记录重链开头的背包和全局背包,每次到一个点先利用全局的背包把轻子节点的背包算出并记录,然后递归重儿子,再递归轻儿子
时间复杂度 \(O(qV)\),空间复杂度 \(O(V\log q)\)
代码咕咕咕了
CF1442D Sum
如果每个序列是上凸的,那就能直接贪心
但如果每个序列是下凸的,那么只会有一个序列没有选满,否则把没选满的一个换成把另一选满更优
于是我们枚举这个没选满的数列选到了哪,然后把剩下的整个序列当作物品,相当于需要知道去掉一个物品后的背包值
可以分块,处理出整块的 DP 值后合并整块,暴力合并散块,但也可以分治,分治到 \([l,r]\) 时表示除了 \([l,r]\) 外其它部分已经做了背包,在递归 \([l,mid]\) 前合并 \([mid+1,r]\) 的背包值,到了叶子节点就是除了它之外其它做背包的值了
时间复杂度 \(O(nk\log n)\)
code
void solve(int l, int r)
{
if(l == r)
{
for(int i = 0; i <= k; ++i) ans = max(ans, f[i] + s[l][k - i]);
return;
}
ll mid = (l + r) >> 1, tmp[M];
for(int i = 0; i <= k; ++i) tmp[i] = f[i];
for(int i = mid + 1; i <= r; ++i)
for(int j = k; j >= num[i]; --j) f[j] = max(f[j], f[j - num[i]] + val[i]);
solve(l, mid);
for(int i = 0; i <= k; ++i) f[i] = tmp[i];
for(int i = l; i <= mid; ++i)
for(int j = k; j >= num[i]; --j) f[j] = max(f[j], f[j - num[i]] + val[i]);
solve(mid + 1, r);
for(int i = 0; i <= k; ++i) f[i] = tmp[i];
}
int main()
{
read(n, k);
for(int i = 1; i <= n; ++i)
{
read(num[i]), a[i].resize(num[i] + 1), s[i].resize(num[i] + 1);
for(int j = 1; j <= num[i]; ++j) read(a[i][j]);
a[i].resize(k + 1), s[i].resize(k + 1);
for(int j = 1; j <= k; ++j) s[i][j] = s[i][j - 1] + a[i][j];
val[i] = s[i][k];
}
solve(1, n);
cout << ans;
return 0;
}
组合计数
CF1667E Centroid Probabilities
推式子数数题
不难发现大小为 \(n\) 的树有 \((n-1)!\) 种
考虑如果一个点子树大小 \(\le mid=\frac{n-1}2\) 怎么算,设当前子树大小为 \(n\),容斥,枚举根不是重心,即它有一棵大小 \(i > \frac{n-1}{2}\) 的子树,那么剩下的 \(n-i-1\) 个点加上根形成树即可
再利用重心的性质,树的重心是深度最深的子树大小 \(>mid\) 的点,假设这个点为 \(x\),枚举它的子树大小为 \(i\),但是要考虑 \(x\) 会接在子树外的哪个点上,由于子树内点的编号都比 \(x\) 大,因此子树外编号比 \(x\) 小的点还是有 \(x-1\) 个,那么总方案数就是
显然可以卷积优化,复杂度 \(O(n\log n)\)
树的构造过程可以看作平常我们随机生成一棵树的过程,利用概率好像有 \(O(n)\) 做法,但我不会
code
int main()
{
cin >> n, mid = (n - 1) >> 1;
fact[0] = invf[0] = 1;
for(ll i = 1; i <= n; ++i) fact[i] = fact[i - 1] * i % mod;
invf[n] = qmi(fact[n], mod - 2);
for(ll i = n - 1; i > 0; --i) invf[i] = invf[i + 1] * (i + 1) % mod;
for(int i = mid + 1; i <= n; ++i) s[i] = add(s[i - 1], invf[i] * fact[i - 1] % mod);
for(int i = mid + 1; i <= n; ++i) f[i] = add(fact[i - 1], mod - fact[i - 1] * s[i - 1] % mod);
a.resize(n + 1), b.resize(n + 2);
for(int i = mid; i < n; ++i) a[n - i + 1] = fact[n - i - 1] * invf[i - 1] % mod * f[i] % mod;
for(int j = 1; j <= n + 1; ++j) b[j] = invf[n - j + 1];
mul(a, b);
print(f[n]), putchar(' ');
// for(int i = 2; i <= mid + 1; ++i) brute force
// {
// ll ans = 0;
// for(int j = mid + 1; j <= n - i + 1; ++j)
// ans = add(ans, c(n - i, j - 1) * f[j] % mod * fact[n - j - 1] % mod * (i - 1) % mod);
// print(ans), putchar(' ');
// }
for(ll i = 2; i < mid + 2; ++i) print(a[n + i + 1] * fact[n - i] % mod * (i - 1) % mod), putchar(' ');
for(int i = mid + 2; i <= n; ++i) print(0), putchar(' ');
return 0;
}
#667. 【UNR #5】提问系统
\(p_r+p_b=n\),因此把式子拆成 \((n-p_b)p_b^2=np_b^2-p_b^3\)
栈的操作序列可以抽象成一棵树,要求每个点到根的链上,\(B\) 的个数在某个范围内
于是可以 DP,需要维护 \(B\) 的 \(0,1,2,3\) 次方,设 \(f(x,i,j)\) 表示在节点 \(x\) 处,到根链上 \(B\) 个数为 \(i\) 时子树内 \(B\) 个数的 \(j\) 次方的总权值和,转移是经典的拆括号转移,细节比较多,复杂度 \(O(n^2)\)
code
void dfs(int x)
{
int st = max(0, dep[x] - lima), ed = min(limb, dep[x]);
for(int i = st; i <= ed; ++i) f[x][i][0] = 1;
for(int y : edge[x])
{
dep[y] = dep[x] + 1, dfs(y);
for(int i = st; i <= ed; ++i) tmp[i][0] = tmp[i][1] = tmp[i][2] = tmp[i][3] = 0;
for(int i = st; i <= ed; ++i)
{
tmp[i][0] = add(f[y][i + 1][0], f[y][i][0]) * f[x][i][0] % mod;
tmp[i][1] = add(add(f[y][i + 1][0], f[y][i][0])* f[x][i][1] % mod, add(f[y][i][1], f[y][i + 1][0], f[y][i + 1][1]) * f[x][i][0] % mod);
tmp[i][2] = add(add(f[y][i + 1][0], f[y][i + 1][1] * 2ll % mod, f[y][i + 1][2], f[y][i][2]) * f[x][i][0] % mod, add(f[y][i + 1][0], f[y][i + 1][1], f[y][i][1]) * f[x][i][1] % mod * 2ll % mod, add(f[y][i + 1][0], f[y][i][0]) * f[x][i][2] % mod);
ll nw3 = add(f[y][i + 1][0], f[y][i + 1][1] * 3ll % mod, f[y][i + 1][2] * 3ll % mod, f[y][i + 1][3], f[y][i][3]) * f[x][i][0] % mod;
ll nw2 = add(f[y][i + 1][0], f[y][i + 1][1] * 2ll % mod, f[y][i + 1][2], f[y][i][2]) * f[x][i][1] % mod * 3ll % mod;
ll nw1 = add(f[y][i + 1][0], f[y][i + 1][1], f[y][i][1]) * f[x][i][2] % mod * 3ll % mod, nw0 = add(f[y][i + 1][0], f[y][i][0]) * f[x][i][3] % mod;
tmp[i][3] = add(nw3, nw2, nw1, nw0);
}
for(int i = st; i <= ed; ++i)
for(int j = 0; j < 4; ++j) f[x][i][j] = tmp[i][j];
}
}
int main()
{
cin >> n >> lima >> limb;
stk[++top] = n + 1;
for(int i = 1; i <= n << 1; ++i)
{
cin >> op;
if(op[1] == 'u') ++idx, edge[stk[top]].pb(idx), stk[++top] = idx, mx = max(mx, top);
else --top;
}
if(lima + limb < top) {cout << 0; return 0;}
dfs(n + 1);
cerr << f[n + 1][0][2] << " " << f[n + 1][0][3] << "\n";
ans = add(1ll * f[n + 1][0][2] * n % mod, mod - f[n + 1][0][3]);
cout << ans;
return 0;
}
[ARC096E] Everything on It
容斥,设 \(f_i\) 表示钦定了 \(i\) 个数出现不多于 \(1\) 次的方案数,则只有剩下 \(n-i\) 个数的集合共 \(2^{n-i}\) 个可以随便选
这 \(i\) 个数枚举在 \(j\) 个集合中,因为还可以不出现,所以很妙的想法是增加一个集合表示不选的数,则共划分为 \(j+1\) 个集合,方案数就是 \(S(i,j)\)
\(j\) 个要选的集合中还可以放其它数,所以每个集合其实有 \(2^{n-i}\) 种,最后二项式反演,求出答案即可,递推求第二类斯特林数,复杂度 \(O(nm)\)
code
int main()
{
cin >> n >> mod;
s[0][0] = 1;
for(int i = 1; i <= n + 1; ++i)
for(ll j = 1; j <= i; ++j) s[i][j] = add(s[i - 1][j - 1], s[i - 1][j] * j % mod);
for(int i = 0; i <= n; ++i)
{
c[i][0] = 1;
for(int j = 1; j <= i; ++j) c[i][j] = add(c[i - 1][j - 1], c[i - 1][j]);
}
for(int i = 0; i <= n; ++i)
{
ll tmp = qmi(2ll, qmi(2ll, n - i, mod - 1), mod), lsh = qmi(2ll, n - i, mod) % mod;
for(int j = 0; j <= i; ++j)
if(i & 1) ans = add(ans, mod - c[n][i] * s[i + 1][j + 1] % mod * tmp % mod * qmi(lsh, j, mod) % mod);
else ans = add(ans, c[n][i] * s[i + 1][j + 1] % mod * tmp % mod * qmi(lsh, j, mod) % mod);
}
cout << ans;
return 0;
}
[AGC008E] Next or Nextnext
将 \(i\) 向 \(a_i\) 连边,得到内向基环森林,将 \(i\) 向 \(p_i\) 连边,得到了若干个置换环,则环的下一个点是原基环森林中的点往前跳最多 \(2\) 步得到
分类讨论基环树的形态,看它能生成的环有多少种
如果本身就是一个环,它可以不变,也可以和另一个同样大小的环拼成大环,拼接时有 \(len\) 种方法,注意大环不能再次拼接,同时如果环长为奇数,则可以每个点都指向后两个,得到新的环,注意新的环也不能拼接
拼接时枚举拼接的对数,计算答案
然后如果是基环树,则不在环上的点都要放到环上,环上每条边最多加一个点,发现点 \(x\) 下放挂的链只能放在 \(x\) 后方的那条边或那条边后面的一条边,最多只有 \(2\) 种放法,且放在后面的第二条边时,后面的第一条边不能放点
这样每条链的摆放都独立,只需要看能不能用第二种放法即可,复杂度 \(O(n)\)
code
void dfs(int x)
{
dfn[x] = ++cnt, stk[++top] = x, tmp.pb(x);
for(int y : edge[x])
if(!dfn[y]) dfs(y);
else if(dfn[y] <= dfn[x] && cir.empty())
{
for(int i = top; i > 0 && stk[i] != y; --i) cir.pb(stk[i]);
cir.pb(y), reverse(cir.begin(), cir.end());
}
--top;
}
void Dfs(int x)
{
siz[x] = 1;
for(int y : edge[x])
if(!vis[y]) Dfs(y), siz[x] += siz[y];
}
ll c(ll x, ll y) {return fact[x] * invf[x - y] % mod * invf[y] % mod;}
int main()
{
read(n);
for(int i = 1; i <= n; ++i) read(a[i]), edge[a[i]].pb(i);
for(int i = 1; i <= n; ++i)
if(edge[i].size() > 2) {cout << 0; return 0;}
for(int i = 1; i <= n; ++i) mul[i] = 1;
fact[0] = qzh[0] = invf[0] = 1;
for(ll i = 1; i <= n; ++i) fact[i] = fact[i - 1] * i % mod;
invf[n] = qmi(fact[n], mod - 2);
for(ll i = n - 1; i > 0; --i) invf[i] = invf[i + 1] * (i + 1) % mod;
for(ll i = 1; i <= n; ++i) qzh[i] = (i & 1) ? qzh[i - 1] * i % mod : qzh[i - 1];
for(int k = 1; k <= n; ++k)
{
if(dfn[k]) continue;
int x = k;
tmp.clear(), cir.clear(), lin.clear(), ++idx, top = 0;
for(; !book[x]; x = a[x]) book[x] = 1;
dfs(x);
for(int u : cir) vis[u] = 1;
for(int u : tmp)
if(!vis[u] && edge[u].size() > 1) {cout << 0; return 0;}
if(tmp.size() - cir.size() > cir.size()) {cout << 0; return 0;}
if(cir.size() == tmp.size())
{
++num[cir.size()], f[idx] = (cir.size() == 1 || (cir.size() % 2 == 0)) ? 1 : 2;
mul[tmp.size()] = mul[tmp.size()] * f[idx] % mod;
continue;
}
f[idx] = 1;
for(int u : cir) Dfs(u);
for(int j = 0; j < 2; ++j)
for(int u : cir) lin.pb(u);
int st = 0;
for(int i = 0; i < lin.size(); ++i)
if(edge[lin[i]].size() > 1) {st = i; break;}
for(int i = st, j = i + 1; i < st + cir.size(); i = j)
{
j = i + 1;
while(j < st + cir.size() && edge[lin[j]].size() <= 1) ++j;
if(j - i < siz[lin[i]] - 1) {cout << 0; return 0;}
else if(j - i > siz[lin[i]] - 1) f[idx] = f[idx] * 2ll % mod;
}
ans = ans * f[idx] % mod;
}
for(int i = 1; i <= n; ++i)
{
ll tot = mul[i];
for(int j = 1; j + j <= num[i]; ++j)
{
ll nw = c(num[i], j + j) * qzh[j + j] % mod * qmi(i, j) % mod;
if(i > 1 && (i & 1)) nw = nw * qmi(2, num[i] - j - j) % mod;
tot = add(tot, nw);
}
ans = ans * tot % mod;
}
cout << ans;
return 0;
}
CF102978H Harsh Comments
每次操作完后选中的概率会变很不妙,转化为选中每个数的概率仍不变,但选中了已经删除的就不耗步数再选
这样每条评论被选的概率不变,其实是求最后一个选中的 \(A_i\) 前选了多少个 \(B_j\),即 \(A_i\) 之前最多有多少个 \(B\)
假设 \(x\) 被选的概率是 \(p\),\(y\) 被选的概率是 \(q\),则 \(x\) 在 \(y\) 之前被选的概率是 \(\sum_{i=0}^{\infty}(1-p-q)^ip=\frac p{p+q}\)
用 min-max 容斥的思想,现在是求 \(E(\max_{i=1}^n A_i\text{前} B \text{的个数} )\),转化为 \(\sum_{S}(-1)^{|S|-1}E(\min_{i\in S} A_i\text{前} B\text{的个数})\)
那么会被统计的 \(B\) 就是必须在集合内所有 \(A\) 之前被选,而一个集合中选到任意一个的概率只和 \(\sum _{i\in S}A_i\) 有关,于是可以背包求出每种 \(\sum A_i\) 对应的 \(S\) 的容斥系数之和,同时每一个 \(B\) 是否在之前是独立的,可以直接分开统计答案
复杂度为 \(O(n\sum A)\)
code
int main()
{
read(n, m);
for(int i = 1; i <= n; ++i) read(a[i]), sum += a[i];
for(int i = 1; i <= m; ++i) read(b[i]);
f[0] = mod - 1;
for(int i = 1; i <= n; ++i) // min-max 容斥,背包对 \sum_{a_i} = x 求出容斥系数 f[x]
for(int j = sum; j >= a[i]; --j) Add(f[j], mod - f[j - a[i]]);
for(int i = 1; i <= m; ++i) // 期望线性,对每个 B_i 单独考虑
for(int j = 1; j <= sum; ++j) Add(ans, b[i] * qmi(add(j, b[i]), mod - 2) % mod * f[j] % mod);
cout << add(ans, n);
return 0;
}
图论
CF1458D Flip and Reverse
观察奇怪的操作,把 \(0\) 看成 \(-1\),发现若要把某个 \(1\) 变成 \(0\),那么翻转区间的末尾也是 \(1\),且这个操作相当于选择两个前缀和相同的位置,翻转中间的区间(不包括右端点)
如果把前缀和看成点,那每次可以选择走到后面的或前面的点,让字典序最小,即尽可能往前走
先建出原序列代表的图,那么我们要遍历图上的每条边,即走一条欧拉路径,翻转相当于是将一个环(欧拉回路)换方向遍历
什么时候能换方向呢?
如果本来在点 \(x\) 我们应该往后走,现在想往前,那么往前到的那个点除了这条边之外必须还有一条边可以回来
这是必要条件,但它也充分
前缀和序列原来形如 \(v, v+1,\dots,v,v-1,\dots,v\),后来变成 \(v,v-1,\dots,v,v+1,\dots,v\),只需要 \(v-1\) 和 \(v\) 之间有 \(\ge 2\) 条边
code
void clear()
{
for(int i = 0; i <= n + n; ++i) sum[i][0] = sum[i][1] = 0;
}
void mian()
{
cin >> (s + 1), n = strlen(s + 1), clear();
for(int i = 1; i <= n; ++i) a[i] = s[i] - '0', qzh[i] = qzh[i - 1] + (a[i] ? 1 : -1);
for(int i = 1; i <= n; ++i) ++sum[qzh[i - 1] + n][a[i]], ++sum[qzh[i] + n][a[i] ^ 1];
int nw = 0;
for(int i = 1; i <= n; ++i)
{
if(sum[nw + n][0] > 0 && (sum[nw + n][1] == 0 || sum[nw + n][0] > 1))
--sum[nw + n][0], putchar('0'), --nw, --sum[nw + n][1];
else --sum[nw + n][1], putchar('1'), ++nw, --sum[nw + n][0];
}
putchar('\n');
}
#670. 【UNR #5】获奖名单
看到这题就有想把字符看成点,长度为 \(2\) 的串两个字符之间有边,然后就不知道怎么办了
最后会形成回文串,先考虑总长度为偶数的情况,那么除了一一对应匹配的,剩下情况就是左侧有一个单个的,然后右侧对应位置是两个的,之后左右都是两个的,最后右边再有一个单个的结束交错
此时这样就对应着一条从单个字符出发,单个字符结束的路径,由于保证有解,因此走这样的尽量多的路径后,剩下的一定一一对应
比较妙的处理是建立虚点 \(s\),这样路径就是从 \(s\) 出发的欧拉回路,跑出欧拉回路即可构造方案,注意特判整个串中间可能是形如 \(AA\) 这样的串
总长度为奇数,就意味着中间的字符不用配对,找到那个字符,如果它是长度为 \(2\) 串的一段,那么找出它出发,到 \(s\) 结尾的一条欧拉路径即可
复杂度 \(O(n+m)\),细节比较多
code
void dfs(int x)
{
for(int &i = st[x]; i < edge[x].size(); ++i)
{
auto [y, w] = edge[x][i];
if(book[abs(w)]) continue;
book[abs(w)] = 1, dfs(y);
path[++cnt] = -w;
}
}
void check()
{
int a[N * 2] = {0}, tmp = 0;
for(int x : zh)
if(x < 0)
for(int i = len[-x] - 1; i >= 0; --i) a[++tmp] = name[-x][i];
else
for(int i = 0; i < len[x]; ++i) a[++tmp] = name[x][i];
for(int i = 1; i <= tmp - i + 1; ++i)
if(a[i] != a[tmp - i + 1]) {cerr << i << " WA.\n"; exit(0);}
cerr << "OK.\n";
}
int main()
{
read(n, m);
for(int i = 1; i <= n; ++i)
{
read(len[i]), tot += len[i];
for(int j = 0; j < len[i]; ++j) read(name[i][j]);
if(name[i][0] < name[i][1]) swap(name[i][0], name[i][1]), tag[i] ^= 1;
if(num[{name[i][0], name[i][1]}] == 0) num[{name[i][0], name[i][1]}] = ++idx;
fr[i] = num[{name[i][0], name[i][1]}], has[fr[i]].pb(i);
if(len[i] == 1) edge[m + 1].pb({name[i][0], i}), edge[name[i][0]].pb({m + 1, i});
else edge[name[i][0]].pb({name[i][1], i}), edge[name[i][1]].pb({name[i][0], -i});
}
dfs(m + 1);
for(int i = 1; i <= n; ++i)
if(!book[i]) ++sum[fr[i]];
if(tot % 2 == 0)
{
for(int i = 1; i <= m; ++i)
if(sum[num[{i, i}]] & 1)
{
for(int j = 1; j <= n; ++j)
if(!book[j] && name[j][0] == i && name[j][1] == i) {zh.pb(j), --sum[fr[j]], book[j] = 1; break;}
break;
}
}
for(int i = 1; i <= cnt; ++i) (i & 1) ? rev.pb(path[i]) : zh.pb(-path[i]);
for(int i = 1; i <= n; ++i)
{
if(book[i] || sum[fr[i]] == 0) continue;
int cur = 0;
for(int x : has[fr[i]])
if(!book[x]) ((cur & 1) ? zh.pb(x) : rev.pb(-x)), book[x] = 1, cur ^= 1;
sum[fr[i]] = 0;
}
reverse(zh.begin(), zh.end());
for(int x : rev) zh.pb(x);
for(int x : zh) print(abs(x)), putchar(' ');
putchar('\n');
for(int x : zh) print(int(x < 0) ^ tag[abs(x)]), putchar(' ');
return 0;
}
#605. 【UER #9】知识网络
发现种类数很少,意味着两点间最短路的长度最多是 \(2k\)(注意这里最短路定义为两点间最短路径的点数)
我们对于每种颜色进行统计,优化建图的方式是建立虚点,它向这种颜色的点连边权为 \(1\) 的有向边,这种颜色的点向它连边权为 $0 $ 的有向边
然后从虚点跑出到所有点的最短路,边权只有 \(0/1\) 可以使用 01 bfs
但是问题是不是所有点到达这种颜色的点都经过虚点,可能是它直接走最短路就能到目标节点,之后再通过虚点到其它点
点 \(x\) 到虚点的最短路上,可能经过的这种颜色的点,真实最短路距离是 \(x\) 到虚点最短路长度 \(-1\)
建出最短路的 DAG,统计每个点的前驱中这种颜色的点的个数,这只能暴力,用 bitset
优化
朴素的每次用长度为 \(n\) 的 bitset
是不行的,因为每做一次复杂度都是 \(O(\frac{nm}w)\),这样总复杂度会是 \(O(k\frac{nm}w)\),就爆炸了
但所有颜色的点个数总和为 \(n\),每次只使用这种颜色点数量大小的 bitset
,复杂度就是 \(O(\frac{nm}w)\)
使用变长 bitset
的技巧会带来空间问题,直接每 \(w\) 个数分一块做,时间复杂度不变,空间复杂度可以做到线性
code
void solve(int st, int nw)
{
queue<int> q;
for(int i = 1; i <= n + k; ++i) book[i] = 0, deg[i] = bac[i];
ull sum = min(nw + 64, tot[st]) - nw;
for(int i = nw; i < nw + 64 && i < tot[st]; ++i) book[ord[st][i]] |= 1llu << (i - nw);
for(int i = 1; i <= n + k; ++i)
if(deg[i] == 0 && dis[i] <= (k << 1)) q.push(i);
while(!q.empty())
{
int x = q.front(); q.pop();
for(auto [y, w] : tu[x])
{
--deg[y], book[y] |= book[x];
if(deg[y] == 0) q.push(y);
}
}
for(int i = 1; i <= n; ++i)
if(dis[i] <= (k << 1))
{
ull tmp = __builtin_popcountll(book[i]);
ans[dis[i]] += tmp, ans[dis[i] + 1] += 1llu * (sum - tmp);
}
}
void work(int st)
{
if(tot[st] == 0) return;
for(int i = 1; i <= n + k; ++i) dis[i] = (k << 1) + 5, tu[i].clear(), deg[i] = vis[i] = 0;
deque<int> q;
dis[n + st] = 0, q.pb(n + st);
while(!q.empty())
{
int x = q.front(); q.pop_front();
if(vis[x]) continue;
vis[x] = 1;
for(auto [y, w] : edge[x])
if(dis[y] > dis[x] + w)
{
dis[y] = dis[x] + w;
w ? q.pb(y) : q.push_front(y);
}
}
for(int x = 1; x <= n + k; ++x)
for(auto [y, w] : edge[x])
if(dis[y] == dis[x] + w) tu[x].pb({y, w}), ++deg[y];
for(int i = 1; i <= n + k; ++i) bac[i] = deg[i];
for(int i = 0; i < tot[st]; i += 64) solve(st, i);
}
int main()
{
read(n, m, k);
for(int i = 1; i <= n; ++i)
{
read(a[i]), ord[a[i]].pb(i), ++tot[a[i]];
edge[a[i] + n].pb({i, 1}), edge[i].pb({a[i] + n, 0});
}
for(int i = 1; i <= m; ++i) read(u, v), edge[u].pb({v, 1}), edge[v].pb({u, 1});
for(int i = 1; i <= k; ++i) work(i);
ull tmp = 0; ans[1] = 0;
for(int i = 1; i <= k * 2; ++i) print(ans[i] >> 1), putchar(' '), tmp += ans[i] >> 1;
print(1llu * n * (n - 1) / 2 - tmp);
return 0;
}
CF1305G Kuroni and Antihype
假设有一个年龄为 \(0\) 的人邀请了所有自愿加入的人,那么整个邀请关系就形成了一棵树,每条边的权值是父节点的点权,想让总边权和最大,且每条边连接两个点点权与为 \(0\)
边权不好处理,但我们把每个点连向父亲的那条边的权值加上它的点权,那么边权形如 \(a_u+a_v\),且总和刚好多了 \(\sum_x a_x\)
剩下的事情就是找出这张图的最大生成树
可以直接跑 Kruskal,直接枚举子集,复杂度为 \(O(3^n)\),常数很小可以通过
但这种题考虑 Boruvka 算法,需要找出跟 \(x\) 不在一个连通块内,\(a_x\ \&\ a_y=0\) 最大的 \(a_y\),
于是考虑转化为求 \(x\ |\ y=x\) 最大的 \(y\),使用 FMT 维护最大值与和最大值不同连通块的最大值,就能完成转移
复杂度为 \(O((n+m2^m)\log n)\)
code
int find(int x) {return x != fa[x] ? fa[x] = find(fa[x]) : x;}
void merge(int x, int y)
{
if(find(x) != find(y)) ans += 1ll * (a[x] + a[y]), fa[find(x)] = find(y);
}
int main()
{
read(n);
for(int i = 1; i <= n; ++i) read(a[i]), sum += 1ll * a[i];
cnt = n + 1, S = (1 << 18) - 1, a[n + 2] = -V * 2;
for(int i = 1; i <= n + 2; ++i) fa[i] = i;
while(cnt > 1)
{
for(int i = 0; i <= S; ++i) f[i] = {n + 2, n + 2};
for(int i = 1; i <= n + 1; ++i) f[a[i]] = {i, n + 2}, book[i] = 0, mx[i] = {n + 2, n + 2};
auto chkmax = [&](pii x, pii y) -> pii
{
if(a[x.fi] < a[y.fi]) swap(x, y);
if(find(y.fi) != find(x.fi)) x.se = a[y.fi] > a[x.se] ? y.fi : x.se;
else if(find(y.se) != find(x.fi)) x.se = a[y.se] > a[x.se] ? y.se : x.se;
return x;
};
for(int j = 0; j <= 17; ++j)
for(int i = 1; i <= S; ++i)
if((i >> j) & 1) f[i] = chkmax(f[i], f[i ^ (1 << j)]);
for(int i = 1; i <= n + 1; ++i)
{
int s = S ^ a[i], x = find(i), y = n + 2;
y = find(f[s].fi) != x ? f[s].fi : f[s].se;
if(y < n + 2 && a[i] + a[y] > a[mx[x].fi] + a[mx[x].se]) mx[x] = {i, y};
}
for(int i = 1; i <= n + 1; ++i)
if(mx[i].fi < n + 2 && mx[i].se < n + 2) merge(mx[i].fi, mx[i].se);
cnt = 0;
for(int i = 1; i <= n + 1; ++i)
{
if(!book[find(i)]) ++cnt;
book[find(i)] = 1;
}
}
cout << ans - sum;
return 0;
}
[ARC103F] Distance Sums
考虑总和最大的一定是叶子,于是可以知道它父亲的权值,由于和两两不同,则直接能知道它的父亲是谁,并同时维护子树大小,从而处理父亲时也能知道它的父亲的和
找出重心然后往下填也可以,不过细节更多,要维护子树内剩余点数,我是这样写的
code
void dfs(int x)
{
siz[x] = 1;
for(int y : edge[x])
dfs(y), f[x] += f[y] + siz[y], siz[x] += siz[y];
}
void Dfs(int x)
{
for(int y : edge[x])
{
ll tmp = f[x] - (f[y] + siz[y]);
f[y] += tmp + n - siz[y];
Dfs(y);
}
}
void check()
{
for(int i = 1; i <= n; ++i) siz[i] = f[i] = 0;
dfs(ord[1]), Dfs(ord[1]);
for(int i = 1; i <= n; ++i)
if(f[i] != dis[i]) {print(-1); exit(0);}
for(int i = 1; i <= n; ++i)
for(int j : edge[i]) print(i), putchar(' '), print(j), putchar('\n');
}
int main()
{
read(n), op = (n & 1) ? 1 : 2;
for(int i = 1; i <= n; ++i) read(dis[i]), mn = min(mn, dis[i]), ord[i] = i;
if(n == 1) {if(dis[1] != 0) print(-1); return 0;}
sort(ord + 1, ord + n + 1, [&](const int x, const int y){return dis[x] < dis[y];});
subt.insert({dis[ord[1]] + op, ord[1]});
if(dis[ord[2]] == dis[ord[1]])
{
if(n & 1) {print(-1); return 0;}
st = 3, siz[ord[1]] = siz[ord[2]] = n / 2 - 1;
subt.insert({dis[ord[2]] + op, ord[2]});
}
else siz[ord[1]] = n - 1;
for(int i = st; i <= n; ++i)
{
int x = ord[i];
iter it = subt.upper_bound({dis[x], N});
if(it == subt.begin()) {print(-1); return 0;}
--it;
edge[it -> se].pb(x);
siz[x] = (n - (dis[x] - dis[it -> se])) / 2 - 1, siz[it -> se] -= siz[x] + 1;
pll tmp = {dis[it -> se] + n - 2 * min(n / 2, siz[it -> se]), it -> se};
subt.erase(*it);
if(siz[tmp.se] > 0) subt.insert(tmp);
if(siz[x] > 0) subt.insert({dis[x] + n - 2 * siz[x], x});
}
check();
return 0;
}
字符串
CF547E Mike and Friends
AC 自动机经典题,转化为 \(s_k\) 在 \(s_{1\sim r}\) 中出现次数减去 \(s_{1\sim l}\) 中出现次数
那么 fail 树上 \(s_k\) 末尾节点的子树内节点所代表的串的后缀都是 \(s_k\),每次加入串 \(s_r\),把它含有的节点权值 \(+1\),那么查询 \(s_k\) 末尾节点的子树和即可
使用树状数组维护,复杂度 \(O(n\log n)\)
code
struct BIT
{
int sum[N], n;
void upd(int x, int val) {for(; x <= n; x += x & (-x)) sum[x] += val;}
int qry(int x) {int res = 0; for(; x; x -= x & (-x)) res += sum[x]; return res;}
int ask(int l, int r) {return qry(r) - qry(l - 1);}
}bit;
struct ACAM
{
int ch[N][28], idx = 1, root = 1, sum[N], fail[N], cnt, dfn[N], mxdfn[N];
vector<int> edge[N];
void insert(int id)
{
int u = root, len = s[id].length();
for(int i = 0; i < len; ++i)
{
if(!ch[u][s[id][i] - 'a']) ch[u][s[id][i] - 'a'] = ++idx;
u = ch[u][s[id][i] - 'a'];
}
ed[id] = u;
}
void dfs(int x)
{
dfn[x] = ++cnt;
for(int y : edge[x]) dfs(y);
mxdfn[x] = cnt;
}
void upd(int id)
{
int u = root, len = s[id].length();
for(int i = 0; i < len; ++i)
{
u = ch[u][s[id][i] - 'a'];
bit.upd(dfn[u], 1);
}
}
void getfail()
{
queue<int> q;
for(int i = 0; i < 26; ++i)
if(ch[root][i]) q.push(ch[root][i]), fail[ch[root][i]] = root;
else ch[root][i] = root;
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = 0; i < 26; ++i)
if(ch[u][i]) fail[ch[u][i]] = ch[fail[u]][i], q.push(ch[u][i]);
else ch[u][i] = ch[fail[u]][i];
}
for(int i = root; i <= idx; ++i) edge[fail[i]].pb(i);
}
}ac;
int main()
{
cin >> n >> q;
for(int i = 1; i <= n; ++i) cin >> s[i], ac.insert(i);
bit.n = ac.idx, ac.getfail(), ac.dfs(ac.root);
for(int i = 1; i <= q; ++i)
{
cin >> l >> r >> k;
ask[l - 1].pb({k, -i}), ask[r].pb({k, i});
}
for(int i = 1; i <= n; ++i)
{
ac.upd(i);
for(pii x : ask[i])
if(x.se > 0) ans[x.se] += bit.ask(ac.dfn[ed[x.fi]], ac.mxdfn[ed[x.fi]]);
else ans[-x.se] -= bit.ask(ac.dfn[ed[x.fi]], ac.mxdfn[ed[x.fi]]);
}
for(int i = 1; i <= q; ++i) print(ans[i]), putchar('\n');
return 0;
}
CF914F Substrings in a String
在中间修改字符显然不好做,只能考虑比较暴力的做法
匹配的过程是一个一个字符的比较,加速这个过程,我们维护当前左端点代表的串还未失配的集合,每次匹配一个字符,找到这个字符存在的位置,并取两个集合的交集
使用 bitset
优化,复杂度 \(O(\frac{nq}w)\)
code
int main()
{
cin >> (s + 1) >> q, n = strlen(s + 1);
for(int i = 1; i <= n; ++i) has[s[i] - 'a'][i] = 1;
for(int i = 1; i <= q; ++i)
{
cin >> op >> l;
if(op == 1)
{
cin >> c;
has[s[l] - 'a'][l] = 0, has[c - 'a'][l] = 1, s[l] = c;
}
else
{
cin >> r >> (t + 1), m = strlen(t + 1);
f.set(), f = (f >> l) << l, f = (f << (N - 1 - (r - m + 1))) >> (N - 1 - (r - m + 1));
for(int j = 1; j <= m; ++j) f &= has[t[j] - 'a'] >> (j - 1);
print(f.count()), putchar('\n');
}
}
return 0;
}
数学
CF1553H XOR and Distance
要对每个 \(x\) 求出 \(\min_{i\ne j}|(a_i\oplus x)-(a_j \oplus x)|\),关于异或的信息,想到按位考虑
设 \(f_{i,j}\) 表示当考虑了前 \(i\) 位且 \(a\) 只有前 \(i\) 位能与 \(x\) 不同时 \(x=j\) 的答案,$ mx_{i,j},mn_{i,j}$ 为此时最大,最小的 \(a\)
转移则考虑新增一位不相同的,在 $x $ 与 \(x \oplus 2^i\) 之间转移,\(mn,mx\) 可以直接更新,考虑 \(f_x\),如果 \(a\) 的第 \(i\) 位不同,则 \(a \oplus x\) 大的那边取最小的,小的那边取最大的,\(mn_{j\oplus 2^i}-mx_{j\oplus 2^i}\),如果相同,则相当于同加同减,差不变,直接用 \(f_{x\oplus 2^i}\) 更新 \(f_x\)
时间复杂度 \(O(n\log n)\)
为啥放到数学板块呢,因为这就是 FWT 迭代 DFT 的过程
其实这个题也可以用 Trie 维护,同样维护当前节点的答案,子树内数减去它代表区间的最小值后的最小值和最大值,异或一个数,相当于对这一位为 \(1\) 的位交换左右子树,它子树内的和祖先的信息其实都没变,只需改变这一层
假设交换从低到高第 \(i\) 位,则复杂度为 \(2^{k-i-1}\),但是如果将 \(x\) 从 \(0\) 开始逐渐增大,那么第 \(i\) 位会变化 \(2^{k-i}\) 次,复杂度依然不对,有个技巧是我们假装枚举的数是翻转的,即第 \(i\) 位是第 \(k-i-1\) 位,那么复杂度就是 \(O(k2^k)\) 了
code
int main()
{
read(n, k);
for(int i = 0; i < (1 << k); ++i) mn[i] = f[i] = N, mx[i] = -N;
for(int i = 1; i <= n; ++i) read(a[i]), mn[a[i]] = mx[a[i]] = 0;
for(int i = 0; i < k; ++i) // 按位加入,每次只考虑只有前 i 位和 x 不同的数,跟某模拟赛 T1 好像,本质上都是 FWT ??? bzd 啊
{
for(int x = 0; x < (1 << k); ++x) tmn[x] = mn[x], tmx[x] = mx[x], g[x] = f[x];
for(int x = 0; x < (1 << k); ++x)
{
int y = x ^ (1 << i);
f[x] = min({f[x], tmn[y] + (1 << i) - tmx[x], g[y]});
mn[x] = min(mn[x], tmn[y] + (1 << i)), mx[x] = max(mx[x], tmx[y] + (1 << i));
}
}
for(int i = 0; i < (1 << k); ++i) print(f[i]), putchar(' ');
return 0;
}
P5339 [TJOI2019] 唱、跳、rap和篮球
容斥原理,枚举钦定有 \(i\) 组不符合,关键是求剩下数的摆放方案数
式子形如
可以用前缀和优化至单次 \(O(n^2)\),但需要单次 \(O(n)\)
先枚举 \(a,b\) 中选的数的总和 \(y\),则
发现枚举 \(y\),后面的就可以用组合数前缀和算出来,注意边界即可
code
ll calc(ll n)
{
ll res = 1, tot = 0;
for(ll i = max(n - a[3] - a[4], 0ll); i <= a[1] + a[2] && i <= n; ++i)
{
ll l1 = max(0ll, i - a[2]), r1 = min(a[1], i);
ll l2 = max(0ll, n - i - a[4]), r2 = min(a[3], n - i);
if(l1 > r1 || l2 > r2) continue;
res = C[n][i] * add(qzh[i][r1], l1 ? mod - qzh[i][l1 - 1] : 0) % mod;
res = res * add(qzh[n - i][r2], l2 ? mod - qzh[n - i][l2 - 1] : 0) % mod;
tot = add(tot, res);
}
return tot;
}
int main()
{
cin >> n >> a[1] >> a[2] >> a[3] >> a[4];
for(int j = 1; j <= 4; ++j) a[j] = min(a[j], n);
C[0][0] = qzh[0][0] = 1;
for(ll i = 1; i <= n; ++i)
{
C[i][0] = qzh[i][0] = 1;
for(ll j = 1; j <= i; ++j) C[i][j] = add(C[i - 1][j - 1], C[i - 1][j]);
for(ll j = 1; j <= i; ++j) qzh[i][j] = add(qzh[i][j - 1], C[i][j]);
}
ans = calc(n);
cerr << ans << "\n";
for(ll i = 1; i <= n / 4; ++i)
{
ll flag = 1;
for(ll j = 1; j <= 4; ++j)
if(a[j] <= 0) {flag = 0; break;}
if(!flag) break;
for(ll j = 1; j <= 4; ++j) --a[j];
ll tot = calc(n - i * 4) * C[n - i * 3][i] % mod;
if(i & 1) ans = add(ans, mod - tot);
else ans = add(ans, tot);
// cerr << tot << "\n";
}
printf("%lld", ans);
return 0;
}
CF908D New Year and Arbitrary Arrangement
想设 \(f(i,j)\) 表示已经有 \(i\) 个子序列且有 \(j\) 个 \(a\) 时的概率,但是可能一直随机到 \(a\) 而没有 \(b\) 导致 \(j\) 这维可能无限大
所以当 \(i+j\ge k\) 时表示再有一个 \(b\) 就能停止,直接求出此时的答案,\(\sum_{x=0}^{\infty}pr_a^xpr_b(i+j+x)\),拆开括号用等比数量求和即可计算
剩下的则是 DP 求出,复杂度 \(O(k^2)\)
code
int main()
{
cin >> k >> pra >> prb;
f[0][1] = 1;
ll lsh = qmi(pra + prb, mod - 2);
pra = pra * lsh % mod, prb = prb * lsh % mod;
for(int i = 0; i < k; ++i)
{
for(int j = 1; j + i < k; ++j)
{
Add(f[i][j + 1], f[i][j] * pra % mod);
Add(f[i + j][j], f[i][j] * prb % mod);
}
}
for(int i = 0; i <= k + k; ++i)
for(int j = 1; j <= k; ++j)
{
if(i >= k) Add(ans, 1ll * f[i][j] * i % mod);
else if(i + j >= k)
{
Add(ans, f[i][j] * prb % mod * qmi(add(1, mod - pra), mod - 2) % mod * (i + j) % mod);
Add(ans, f[i][j] * prb % mod * pra % mod * qmi(add(1, mod - pra) * add(1, mod - pra) % mod, mod - 2) % mod);
}
}
cout << ans;
return 0;
}
CF103415J Cafeteria
之前模拟赛搬过加强版本,这里就是用转置原理快速对系数矩阵求逆,把每个位置的值看成对一个向量的线性变换,乘法就是变换过去,求逆后相当于逆变换
code
int main()
{
cin >> n >> m >> q >> (s + 1) >> (t + 1);
for(int j = 0; j <= m; ++j) f[j][j] = g[j][j] = 1;
inv[0][0] = 1;
for(int i = 1; i <= n; ++i)
{
for(int j = m; j > 0; --j)
{
if(t[j] != s[i]) continue;
for(int k = 0; k <= m; ++k) Add(f[k][j], f[k][j - 1]);
for(int k = 0; k <= m; ++k) Add(g[k][j - 1], mod - g[k][j]);
}
for(int j = 0; j <= m; ++j) mul[i][j] = f[j][m], inv[i][j] = g[j][0];
}
for(int i = 1; i <= q; ++i)
{
cin >> l >> r;
ll sum = 0;
for(int j = 0; j <= m; ++j) Add(sum, mul[r][j] * inv[l - 1][j] % mod);
ans ^= sum;
}
cout << ans;
return 0;
}