2024.02 别急记录
1. WC/CTSC2024 - 水镜 [省选/NOI-]
设 \(2L=T\),我们可以发现相邻两项之间的大小关系有四种:
- \(h_i<h_{i+1}\);
- \(h_i<T-h_{i+1}\) 即 \(h_i+h_{i+1}<T\);
- \(T-h_i<T-h_{i+1}\) 即 \(h_i>h_{i+1}\);
- \(T-h_i<h_{i+1}\) 即 \(h_i+h_{i+1}>T\)。
那么由 1,2 可得若 \(h_i\geq h_{i+1},h_i+h_{i+1}\geq T\) 则一定会取 \(T-h_i\)。
类似地总共四条:
- 若 \(h_i\geq h_{i+1},h_i+h_{i+1}\geq T\) 则一定会取 \(T-h_i\);
- 若 \(h_i\geq h_{i+1},h_i+h_{i+1}\leq T\) 则一定会取 \(T-h_{i+1}\);
- 若 \(h_i\leq h_{i+1},h_i+h_{i+1}\geq T\) 则一定会取 \(h_{i+1}\);
- 若 \(h_i\leq h_{i+1},h_i+h_{i+1}\leq T\) 则一定会取 \(h_i\)。
那么我们接着考虑 \(h_{i-1},h_i,h_{i+1}\) 三者的关系,可得若 \(h_{i-1},h_i\) 满足上述条件 2,\(h_i,h_{i+1}\) 满足上述条件 4,则 \(h_i\) 既要取 \(h_i\) 又要取 \(T-h_i\),所以这个 \(T\) 对于 \(h_i\) 不可行。
那么我们就可以对于每个 \(i\) 求出 \((L_i,R_i)\) 表示区间里有这个 \(i\) 会对 \(T\) 产生的限制:
- 若 \(h_i\geq\max(h_{i-1},h_{i+1}),T\geq \min(h_{i-1}, h_{i+1}) + h_i\);
- 若 \(h_i\leq\min(h_{i-1},h_{i+1}),T\leq \max(h_{i-1}, h_{i+1}) + h_i\);
那么一个区间合法当且仅当 \(\max L_i < \min R_i\)。可以枚举左端点并二分右端点求解。
点击查看代码
const int N = 5e5 + 10;
ll lw[20][N], up[20][N], h[N];
int n;
bool chk(int L, int R){
if(L > R){
return 1;
}
int k = 31 ^ __builtin_clz(R-L+1);
ll Lm = max(lw[k][L], lw[k][R-(1<<k)+1]);
ll Rm = min(up[k][L], up[k][R-(1<<k)+1]);
return Lm < Rm;
}
void solve(){
read(n);
for(int i = 1; i <= n; ++ i){
read(h[i]);
}
lw[0][1] = lw[0][n] = -1e18;
up[0][1] = up[0][n] = 1e18;
for(int i = 2; i < n; ++ i){
lw[0][i] = -1e18;
up[0][i] = 1e18;
if(h[i-1] <= h[i] && h[i] >= h[i+1]){
lw[0][i] = min(h[i-1], h[i+1]) + h[i];
}
if(h[i-1] >= h[i] && h[i] <= h[i+1]){
up[0][i] = max(h[i-1], h[i+1]) + h[i];
}
}
for(int i = 1; i < 20; ++ i){
for(int j = 1; j + (1 << i) - 1 <= n; ++ j){
lw[i][j] = max(lw[i-1][j], lw[i-1][j+(1<<i-1)]);
up[i][j] = min(up[i-1][j], up[i-1][j+(1<<i-1)]);
}
}
ll ans = 0;
for(int i = 1; i <= n; ++ i){
int l = i, r = n;
while(l < r){
int mid = l + r + 1 >> 1;
if(chk(i + 1, mid - 1)){
l = mid;
} else {
r = mid - 1;
}
}
ans += l - i;
}
println(ans);
}
2. CTSC2017 - 吉夫特 [省选/NOI-]
简单题啊。
一个经典结论是 \(\dbinom nm \equiv [n\operatorname{AND} m=m]\pmod 2\)
proof
根据卢卡斯定理有 \(\dbinom nm\equiv \dbinom{n/p}{m/p}\dbinom{n\bmod p}{m\bmod p}\)。
然后我们观察到如果 \(n\) 最低位是 \(0\) 而 \(m\) 最低位是 \(1\),那么这个就是 \(0\) 了。
否则这个不会变为 \(0\) 就是 \(1\)。
然后就能写出转移方程:\(f_i=\sum_{a_i\operatorname{AND}a_j=a_i}\{f_j+1\}\),使用枚举子集即可 \(O(3^{\log V})\) 转移。
点击查看代码
const int N = 262144 + 10;
const ll P = 1e9 + 7;
int n, a[N], p[N];
ll f[N], ans;
void solve(){
read(n);
memset(p, 0x3f, sizeof(p));
for(int i = 1; i <= n; ++ i){
read(a[i]);
p[a[i]] = i;
}
for(int i = 0; i <= n; ++ i){
int s = 262143 - a[i];
for(int j = s; j > 0; j = (j - 1) & s){
f[a[i]] = (f[a[i]] + (p[a[i]+j] < i ? f[a[i]+j]+1 : 0)) % P;
}
ans = (ans + f[a[i]]) % P;
}
println(ans);
}
3. CF1918G - Permutation of Given [*2700]
无聊构造题。
首先 \(n=3,5\) 是无解的。
proof
当 $n=3$ 时,设序列为 $\{a,b,c\}$,变化后为 $\{b,a+c,b\}$,显然二者总和不等,故无解。当 \(n=5\) 时,设序列为 \(\{a,b,c,d,e\}\),变化后为 \(\{b,a+c,b+d,c+e,d\}\),\(b=b\),\(d=d\),\(a+c\neq a,c\) 只能 \(=e\),\(c+e=a\),所以 \(b+d=c\);又二者总和相减得 \(b+c+d=0\),所以 \(c=0\),无解。
然后考虑一个合法的长度为 \(n-2\) 的序列,设其最后两项为 \(a,b\),变化后的最后两项为 \(t,a\)。
那么若在序列末尾添加 \(x,y\),则 \(\{a,b,x,y\}=\{t,a+x,b+y,x\}\),解得可取 \(x=-b,y=a-b\)。
那么我们只用找到 \(n=2\) 和 \(n=7\) 时的解即可。
\(n=2\) 很好找:\(\{1,2\}\)。
\(n=7\) 打表可得 \(\{-3,-3,2,1,-1,1,-2\}\)。
点击查看代码
const int N = 1e6 + 10;
int n, a[N];
void solve(){
read(n);
if(n == 3 || n == 5){
println_cstr("NO");
} else if(n & 1){
println_cstr("YES");
a[1] = a[2] = -3;
a[3] = 2;
a[4] = a[6] = 1;
a[5] = -1;
a[7] = -2;
for(int i = 8; i <= n; i += 2){
a[i] = - a[i-1];
a[i+1] = a[i-2] - a[i-1];
}
for(int i = 1; i <= n; ++ i){
printk(a[i]);
}
} else {
println_cstr("YES");
a[1] = 1;
a[2] = 2;
for(int i = 3; i <= n; i += 2){
a[i] = - a[i-1];
a[i+1] = a[i-2] - a[i-1];
}
for(int i = 1; i <= n; ++ i){
printk(a[i]);
}
}
}
4. COCI2018-2019#4 - Akvizna [NOI/NOI+/CTSC]
wqs 二分+斜率优化。
首先考虑如果没有 \(k\) 的限制该怎么做:倒着做,设 \(f_i\) 表示赢后 \(i\) 个选手的最多奖金,有转移:
\(f_i=\max\{f_j+\dfrac{i-j}j\}=\max\{\dfrac 1i * j + f_j - 1\}\)
可以斜率优化。具体地,用单调队列维护 \((j,f_j-1)\) 的上凸壳,斜线斜率是 \(\dfrac 1i\) 由大到小,故凸壳斜率递减。
然后加上 \(k\) 的限制就是 wqs 二分。二分斜率 \(k_0\),用这条直线去切答案下凸壳,切到的点是 \((k,f_{n,k})\) 即可取答案。二分后的答案应尽量大,所以当可行时 \(mid\to l\)。
点击查看代码
const int N = 1e5 + 10;
const double eps = 1e-16;
int n, k, q[N], g[N];
double f[N];
double slp(int x, int y){
return (f[x] - f[y]) * 1.0 / (x - y);
}
bool chk(double x){
int l = 1, r = 0;
q[1] = 0;
memset(g, 0, sizeof(g));
for(int i = 1; i <= n; ++ i){
while(l < r && slp(q[l+1], q[l]) > 1.0 / i + eps){
++ l;
}
f[i] = f[q[l]] + (i - q[l]) * 1.0 / i - x;
g[i] = g[q[l]] + 1;
while(l < r && slp(q[r], q[r-1]) + eps < slp(i, q[r])){
-- r;
}
q[++r] = i;
}
return g[n] >= k;
}
void solve(){
scanf("%d%d", &n, &k);
double l = 0, r = 1e6;
while(l + eps < r){
double mid = (l + r) / 2;
if(chk(mid)){
l = mid;
} else {
r = mid;
}
}
printf("%.9lf\n", f[n] + l * k);
}
5. LuoguP5633 - 最小度限制生成树 [省选/NOI-]
wqs 二分。
考虑给每条连接到 \(s\) 的边 \(-x\),然后二分最小生成树中 \(s\) 度数 \(\geq k\) 的最小 \(x\)。
无解情况:\(s\) 本身度数 \(<k\);图不连通;\(x=-inf\) 时度数仍 \(>k\)。
点击查看代码
const int N = 1e5 + 10, M = 5e5 + 10;
int n, m, s, k, deg, fa[N], tx, ty;
ll res;
struct edge{
int u, v, w;
} e[M], E[M];
bool cmp(edge x, edge y){
return x.w < y.w;
}
int gf(int x){
return x == fa[x] ? x : fa[x] = gf(fa[x]);
}
int chk(int x){
for(int i = 1; i <= n; ++ i){
fa[i] = i;
}
ll ans = 0, cnt = 0, i = 1, j = 1;
while(i <= tx || j <= ty){
if(i <= tx && (e[i].w < E[j].w - x || j > ty)){
if(gf(e[i].u) != gf(e[i].v)){
fa[gf(e[i].u)] = gf(e[i].v);
ans += e[i].w;
}
++ i;
} else {
if(gf(E[j].u) != gf(E[j].v)){
fa[gf(E[j].u)] = gf(E[j].v);
ans += E[j].w - x;
++ cnt;
}
++ j;
}
}
res = ans;
return cnt;
}
void solve(){
read(n, m, s, k);
for(int i = 1; i <= n; ++ i){
fa[i] = i;
}
for(int i = 1; i <= m; ++ i){
int u, v, w;
read(u, v, w);
fa[gf(u)] = gf(v);
if(u == s || v == s){
++ deg;
E[++ty] = { u, v, w };
} else {
e[++tx] = { u, v, w };
}
}
sort(e + 1, e + tx + 1, cmp);
sort(E + 1, E + ty + 1, cmp);
int cnt = 0;
for(int i = 1; i <= n; ++ i){
if(gf(i) == i){
++ cnt;
}
}
if(deg < k || cnt > 1 || chk(-1e5) > k){
println_cstr("Impossible");
return;
}
int l = -1e5, r = 1e5;
while(l < r){
int mid = l + r >> 1;
if(chk(mid) >= k){
r = mid;
} else {
l = mid + 1;
}
}
chk(l);
println(res + k * l);
}
6. POI2014 - PAN-Solar Panels [提高+/省选-]
显然可以四维数论分块。
但是发现对于 \(\lfloor\dfrac bx\rfloor,\lfloor\dfrac dx\rfloor\) 相同的 \(x\in[l,r]\),\(x=r\) 一定是其中最优的。于是可以二维数论分块解决。
点击查看代码
int T, a, b, c, d;
void solve(){
read(T);
while(T--){
read(a, b, c, d);
if(b > d){
swap(a, c);
swap(b, d);
}
int ans = 0;
for(int l = 1, r; l <= b; l = r + 1){
r = min(b / (b / l), d / (d / l));
if((a-1) / r < b / r && (c-1) / r < d / r){
ans = max(ans, r);
}
}
println(ans);
}
}
7. Ynoi ER 2021 - TEST_152 [省选/NOI-]
考虑从左往右执行操作的同时使用树状数组记录:第 \(i\) 位记录最后一次操作是操作 \(i\) 的所有位置的值之和。然后把询问离线并绑定到右端点,执行完操作 \(r\) 后询问 \([l,r]\) 的答案即为树状数组的 \([l,r]\) 之和。
接着考虑如何快速操作:使用 set
维护序列连续段及其值、最后修改的时间,然后每次修改找到那些与之有交的序列段进行修改。可以发现每次至多增加两段,故总段数是 \(O(n)\) 的。注意找序列段的实现。
复杂度 \(O(n\log n)\)。
点击查看代码
const int N = 5e5 + 10;
int n, m, q, L[N], R[N], v[N];
vector<pair<int, int> > qr[N];
tuple<int, int, int> seq[N];
ll bit[N], ans[N];
void add(int x, ll v){
++ x;
while(x <= n + 1){
bit[x] += v;
x += x & -x;
}
}
ll ask(int x){
++ x;
ll rs = 0;
while(x){
rs += bit[x];
x -= x & -x;
}
return rs;
}
ll ask(int l, int r){
return ask(r) - ask(l-1);
}
void solve(){
read(n, m, q);
for(int i = 1; i <= n; ++ i){
read(L[i], R[i], v[i]);
}
for(int i = 1; i <= q; ++ i){
int l, r;
read(l, r);
qr[r].emplace_back(l, i);
}
set<int> st;
st.insert(1);
seq[1] = { n, 0, 0 };
for(int i = 1; i <= n; ++ i){
while(true){
auto it = st.upper_bound(R[i]);
if(it == st.begin()){
break;
}
-- it;
int l = (*it), r, cl, vl;
tie(r, cl, vl) = seq[l];
if(L[i] <= l && r <= R[i]){
add(cl, -(ll)vl * (r-l+1));
st.erase(it);
} else if(L[i] <= l && R[i] < r && l <= R[i]){
add(cl, -(ll)vl * (R[i]-l+1));
st.erase(it);
st.insert(R[i]+1);
seq[R[i]+1] = { r, cl, vl };
} else if(l < L[i] && r <= R[i] && L[i] <= r){
add(cl, -(ll)vl * (r-L[i]+1));
seq[l] = { L[i]-1, cl, vl };
break;
} else if(l < L[i] && R[i] < r){
add(cl, -(ll)vl * (R[i]-L[i]+1));
seq[l] = { L[i]-1, cl, vl };
st.insert(R[i]+1);
seq[R[i]+1] = {r, cl, vl};
break;
} else {
break;
}
}
st.insert(L[i]);
seq[L[i]] = { R[i], i, v[i] };
add(i, (ll)v[i] * (R[i]-L[i]+1));
for(auto j : qr[i]){
ans[j.second] = ask(j.first, i);
}
}
for(int i = 1; i <= q; ++ i){
println(ans[i]);
}
}
8. CF627F - Island Puzzle [*3400]
思路不难的逆天角盒题!
首先考虑不加边的情况,容易发现将 \(0\) 的位置由起始一步一步移动到目标是唯一的操作方法。那么就先这样进行一下操作,如果已经完成目标就是不用加边的最短步骤;否则一定需要加边。
那么将 \(0\) 放对位置后,还剩下若干 \(a\neq b\) 的位置。我们可以发现 \(0\) 在环上绕圈相当于其他位置进行一个轮换。那么求出这些点的 lca \(p\)。若 \(p\) 与这些点构成一条链,那么就要把链加一条边连成环;否则无解。然后如果环上除掉 \(p\) 以外的点 \(a,b\) 不构成轮换,无解;否则,步骤为 \(0\) 初始位置 \(\to 0\) 目标位置 \(\to p\),然后顺时针或逆时针绕圈,最后返回 \(0\) 目标位置。
但是,有可能有的路径可以省去!比如 \(p\) 在 \(0\) 初始位置 \(\to 0\) 目标位置上,就不用先到 \(0\) 目标位置,而是直接去绕圈。这个对于顺时针和逆时针两种情况分别判一下,因为第一圈的某些步一样可以省去,最后取最小值即可。
点击查看代码
const int N = 2e5 + 10;
int n, a[N], b[N], a0, b0;
vector<int> g[N];
int st[N], tp;
bool dfs1(int x, int fa){
st[++tp] = x;
if(x == b0){
return 1;
}
for(int i : g[x]){
if(i != fa){
if(dfs1(i, x)){
return 1;
}
}
}
-- tp;
return 0;
}
int nd[N], dep[N], fat[N], ind[N];
void dfs2(int x, int fa){
fat[x] = fa;
dep[x] = dep[fa] + 1;
for(int i : g[x]){
if(i != fa){
dfs2(i, x);
}
}
}
int stt[N], tpp, as[N], bs[N], so[N];
bool dfs3(int x, int fa, int gl){
stt[++tpp] = x;
if(x == gl){
return 1;
}
for(int i : g[x]){
if(i != fa && nd[i]){
if(dfs3(i, x, gl)){
return 1;
}
}
}
-- tpp;
return 0;
}
void solve(){
read(n);
for(int i = 1; i <= n; ++ i){
read(a[i]);
if(a[i] == 0){
a0 = i;
}
}
for(int i = 1; i <= n; ++ i){
read(b[i]);
if(b[i] == 0){
b0 = i;
}
}
for(int i = 1; i < n; ++ i){
int u, v;
read(u, v);
g[u].push_back(v);
g[v].push_back(u);
}
dfs1(a0, 0);
for(int i = 1; i < tp; ++ i){
swap(a[st[i]], a[st[i+1]]);
}
bool flg = 1;
for(int i = 1; i <= n; ++ i){
if(a[i] != b[i]){
flg = 0;
break;
}
}
if(flg){
println(0, tp - 1);
return;
}
dfs2(b0, 0);
int p = 0;
bool ok = 1;
for(int i = 1; i <= n; ++ i){
if(a[i] != b[i]){
nd[i] = 1;
}
}
for(int i = 1; i <= n; ++ i){
if(nd[i]){
if(!p || dep[p] >= dep[i]){
p = fat[i];
}
}
}
nd[p] = 1;
for(int i = 1; i <= n; ++ i){
for(int j : g[i]){
if(i < j && nd[i] && nd[j]){
++ ind[i];
++ ind[j];
}
}
}
int c = 0;
for(int i = 1; i <= n; ++ i){
if(ind[i] == 1){
++ c;
} else if(nd[i] && ind[i] != 2){
ok = 0;
break;
}
}
if(c != 2){
ok = 0;
}
if(!ok){
println(-1);
return;
}
int u = 0, v = 0;
for(int i = 1; i <= n; ++ i){
if(ind[i] == 1){
if(u){
v = i;
} else {
u = i;
}
}
}
dfs3(u, 0, v);
int psp = 0;
for(int i = 1, q = 0; i <= tpp; ++ i){
if(stt[i] != p){
as[++q] = a[stt[i]];
bs[q] = b[stt[i]];
} else {
psp = i;
}
}
int dy = 0;
for(int i = 1; i < tpp; ++ i){
if(as[1] == bs[i]){
dy = i;
for(int j = 1, k = i; j < tpp; ++ j, ++ k){
if(k == tpp){
k = 1;
}
if(as[j] != bs[k]){
ok = 0;
break;
}
}
break;
}
}
if(dy == 0){
ok = 0;
}
if(!ok){
println(-1);
return;
}
int tmp = p;
while(tmp != b0){
so[fat[tmp]] = tmp;
tmp = fat[tmp];
}
ll ac = 2, rc = 2;
ac += tp - 1 + (dep[p] - 1) * 2;
rc += tp - 1 + (dep[p] - 1) * 2;
ac += (dy - 1) * 1ll * tpp;
rc += (tpp - dy) * 1ll * tpp;
int np = b0, nq = b0, fl = tp, pr = 0;
while(true){
if(np == nq){
-- rc;
-- rc;
}
if(np == a0 || nq == stt[tpp]){
break;
}
-- fl;
np = st[fl];
if(nq == p){
pr = psp;
}
if(!pr){
nq = so[nq];
} else {
++ pr;
nq = stt[pr];
}
}
np = b0, nq = b0, fl = tp, pr = 0;
while(true){
if(np == nq){
-- ac;
-- ac;
}
if(np == a0 || nq == stt[1]){
break;
}
-- fl;
np = st[fl];
if(nq == p){
pr = psp;
}
if(!pr){
nq = so[nq];
} else {
-- pr;
nq = stt[pr];
}
}
println(u, v, min(ac, rc));
}
9. COCI2021-2022#2 - Osumnjičeni [省选/NOI-]
双指针+线段树可以求出对于每个人,最右边能选到哪个人。然后倍增即可。
点击查看代码
const int N = 2e5 + 10, M = 4e5 + 10;
int n, q, l[N], r[N], pl[M], tp, t[M*4], tag[M*4], f[N][20];
void psd(int p){
if(tag[p] == -1){
return;
}
t[p<<1] = t[p<<1|1] = tag[p<<1] = tag[p<<1|1] = exchange(tag[p], -1);
}
void add(int p, int l, int r, int ql, int qr, int v){
if(qr < l || r < ql){
return;
} else if(ql <= l && r <= qr){
t[p] = tag[p] = v;
} else {
int mid = l + r >> 1;
psd(p);
add(p<<1, l, mid, ql, qr, v);
add(p<<1|1, mid+1, r, ql, qr, v);
t[p] = max(t[p<<1], t[p<<1|1]);
}
}
int qry(int p, int l, int r, int ql, int qr){
if(qr < l || r < ql){
return 0;
} else if(ql <= l && r <= qr){
return t[p];
} else {
int mid = l + r >> 1;
psd(p);
return max(qry(p<<1, l, mid, ql, qr), qry(p<<1|1, mid+1, r, ql, qr));
}
}
void solve(){
read(n);
memset(tag, -1, sizeof(tag));
for(int i = 1; i <= n; ++ i){
read(l[i], r[i]);
pl[++tp] = l[i];
pl[++tp] = r[i];
}
sort(pl + 1, pl + tp + 1);
tp = unique(pl + 1, pl + tp + 1) - pl - 1;
for(int i = 1; i <= n; ++ i){
l[i] = lower_bound(pl + 1, pl + tp + 1, l[i]) - pl;
r[i] = lower_bound(pl + 1, pl + tp + 1, r[i]) - pl;
}
int L = 1, R = 0;
while(R < n && qry(1, 1, tp, l[R+1], r[R+1]) == 0){
add(1, 1, tp, l[R+1], r[R+1], 1);
++ R;
}
f[1][0] = R + 1;
for(L = 2; L <= n; ++ L){
add(1, 1, tp, l[L-1], r[L-1], 0);
while(R < n && qry(1, 1, tp, l[R+1], r[R+1]) == 0){
add(1, 1, tp, l[R+1], r[R+1], 1);
++ R;
}
f[L][0] = R + 1;
}
f[n+1][0] = n + 1;
for(int i = 1; i < 20; ++ i){
for(int j = 1; j <= n + 1; ++ j){
f[j][i] = f[f[j][i-1]][i-1];
}
}
read(q);
while(q--){
int x, y, ans = 0;
read(x, y);
for(int i = 19; i >= 0; -- i){
if(f[x][i] <= y){
ans += (1 << i);
x = f[x][i];
}
}
println(ans + 1);
}
}
10. BJOI2014 - 想法 [省选/NOI-]
非常厉害!
发现直接做不好做。我们考虑给每个 \([1,m]\) 之间的点附一个随机权值,然后求每个点所能到达的最小权值,设其为 \(s\),则 \(s\) 的期望为 \(\dfrac{RAND\_MAX}{k+1}\),其中 \(k\) 为所能到达点的数量。那么我们就可以多跑几次来估计出 \(k\)。
点击查看代码
const int N = 2e6 + 10, T = 100;
int c[N][2], f[N][T], n, m;
double ans[N];
void solve(){
read(n, m);
for(int i = m + 1; i <= n; ++ i){
read(c[i][0], c[i][1]);
}
for(int p = 0; p <= 2; ++ p){
for(int i = 1; i <= m; ++ i){
for(int j = 0; j < T; ++ j){
f[i][j] = rand();
}
}
for(int i = m + 1; i <= n; ++ i){
for(int j = 0; j < T; ++ j){
f[i][j] = min(f[c[i][0]][j], f[c[i][1]][j]);
ans[i] += f[i][j];
}
}
}
for(int i = m + 1; i <= n; ++ i){
println((int)(RAND_MAX / ans[i] * T * 3 - 0.5));
}
}
11. CF809E - Surprise me! [*3100]
虚树+推式子。
我们有 \(\varphi(xy) = \dfrac{\varphi(x)\varphi(y)(x,y)}{\varphi((x,y))}\)
proof
\(\varphi(n)\varphi(m)=n\prod_{i|n}(\frac{i-1}i)m\prod_{j|m}(\frac{j-1}j)\)
稍微变形,
\(=nm\prod_{i|nm}(\frac{i-1}i)\prod_{j|(n,m)}(\frac{j-1}j)=\varphi(nm)\frac{\gcd(n,m)}{\varphi(\gcd(n,m))}\)
所以原式可以变为:
枚举 \(\gcd\):
设:
等于不好算,换成倍数,设:
反演:
考虑计算 \(F(d)\)。把树上所有 \(d|a_i\) 的点拎出来建虚树,消掉中括号。有 \(\operatorname{dist}(i,j)=dep_i+dep_j-2dep_{\operatorname{lca}(i,j)}\),所以:
变形,得:
前面的直接处理,后面的可以 dp 的同时在 \(lca\) 处统计答案。
点击查看代码
const int N = 4e5 + 10;
const ll P = 1e9 + 7;
int n, a[N], v[N], ps[N];
ll F[N], f[N];
namespace Sieve{
int p[N], v[N], c, phi[N], mu[N];
void clc(int n){
phi[1] = mu[1] = 1;
for(int i = 2; i <= n; ++ i){
if(!v[i]){
p[++c] = i;
phi[i] = i - 1;
mu[i] = P - 1;
}
for(int j = 1; j <= c && i * p[j] <= n; ++ j){
v[i*p[j]] = 1;
if(i % p[j]){
phi[i*p[j]] = phi[i] * phi[p[j]];
mu[i*p[j]] = P - mu[i];
} else {
phi[i*p[j]] = phi[i] * p[j];
mu[i*p[j]] = 0;
break;
}
}
}
}
ll inv(ll x){
ll ans = 1, y = P - 2;
while(y){
if(y & 1){
ans = ans * x % P;
}
x = x * x % P;
y >>= 1;
}
return ans;
}
}
namespace VirtualTree{
int dfn[N], dfc, dep[N], st[20][N];
vector<int> g[N];
int get(int x, int y){
return dfn[x] < dfn[y] ? x : y;
}
void dfs(int x, int fa){
dfn[x] = ++ dfc;
st[0][dfn[x]] = fa;
dep[x] = dep[fa] + 1;
for(int i : g[x]){
if(i != fa){
dfs(i, x);
}
}
}
void init(){
dfs(1, 0);
for(int i = 1; i < 20; ++ i){
for(int j = 1; j + (1<<i) - 1 <= n; ++ j){
st[i][j] = get(st[i-1][j], st[i-1][j+(1<<i-1)]);
}
}
}
int lca(int u, int v){
if(u == v){
return u;
}
if((u = dfn[u]) > (v = dfn[v])){
swap(u, v);
}
int d = 31 ^ __builtin_clz(v - u ++);
return get(st[d][u], st[d][v-(1<<d)+1]);
}
int h[N], m, a[N], len, val[N];
vector<pair<int, int> > G[N];
set<int> s;
int DEP[N];
bool cmp(int x, int y){
return dfn[x] < dfn[y];
}
void bld(){
h[++m] = 1;
sort(h + 1, h + m + 1, cmp);
len = 0;
bool is1 = 0;
for(int i = 1; i < m; ++ i){
a[++len] = h[i];
a[++len] = lca(h[i], h[i+1]);
}
for(int i = 2; i <= m; ++ i){
val[h[i]] = v[h[i]];
}
a[++len] = h[m];
sort(a + 1, a + len + 1, cmp);
len = unique(a + 1, a + len + 1) - a - 1;
for(int i = 1; i < len; ++ i){
int lc = lca(a[i], a[i+1]);
G[lc].emplace_back(a[i+1], dep[a[i+1]] - dep[lc]);
s.insert(lc);
s.insert(a[i]);
}
s.insert(a[len]);
}
ll siz[N], x, y, z;
void clr(){
for(int i : s){
val[i] = siz[i] = DEP[i] = 0;
vector<pair<int, int> > ().swap(G[i]);
}
s.clear();
m = len = 0;
}
void dfss(int x, int fa){
for(auto i : G[x]){
int y = i.first;
DEP[y] = DEP[x] + i.second;
dfss(y, x);
z = (z + siz[x] * siz[y] % P * DEP[x]) % P;
siz[x] += siz[y];
}
z = z + (siz[x] * val[x] % P * DEP[x]) % P;
siz[x] = (siz[x] + val[x]) % P;
}
ll solve(){
bld();
x = 0, y = 0, z = 0;
dfss(1, 0);
z = z * 2 % P;
for(int i = 1; i <= len; ++ i){
x = (x + (ll)val[a[i]] * DEP[a[i]]) % P;
y = (y + val[a[i]]) % P;
z = (z + (ll)val[a[i]] * val[a[i]] % P * DEP[a[i]]) % P;
}
clr();
return ((2 * x * y - 2 * z) % P + P) % P;
}
}
void solve(){
read(n);
Sieve::clc(n);
for(int i = 1; i <= n; ++ i){
read(a[i]);
v[i] = Sieve::phi[a[i]];
ps[a[i]] = i;
}
for(int i = 1; i < n; ++ i){
int u, v;
read(u, v);
VirtualTree::g[u].push_back(v);
VirtualTree::g[v].push_back(u);
}
VirtualTree::init();
for(int x = 1; x <= n; ++ x){
VirtualTree::m = 0;
for(int i = x; i <= n; i += x){
VirtualTree::h[++VirtualTree::m] = ps[i];
}
F[x] = VirtualTree::solve();
}
for(int x = 1; x <= n; ++ x){
for(int d = x; d <= n; d += x){
f[x] = (f[x] + F[d] * Sieve::mu[d/x]) % P;
}
}
ll ans = 0;
for(int d = 1; d <= n; ++ d){
ans = (ans + d * Sieve::inv(Sieve::phi[d]) % P * f[d]) % P;
}
println(ans * Sieve::inv((ll)n * (n-1) % P) % P);
}
12. NEERC2017 - Journey from Petersburg to Moscow [省选/NOI-]
发现 \(n,m\leq 3000\),考虑枚举第 \(k\) 大边权值 \(W\)。
对于枚举的 \(W\),令每条边权值 \(w'=\max(0,w-W')\),然后跑最短路,用 \(\operatorname{MinDist}(1,n)+kW\) 更新答案。
考虑为什么是对的:
- 对于第 \(k\) 大权值正好是 \(W\) 的路径,显然最后算到的答案相同;
- 对于第 \(k\) 大权值大于 \(W\) 的路径,答案会多加上 \([W+1,w']\) 这一部分边的权值与 \(W\) 差值的和,更大;
- 对于第 \(k\) 大权值小于 \(W\) 的路径,答案会多加上 \([w'+1,W]\) 这一部分边的权值与 \(W\) 差值的和,更大。
故一定能找到最小解。
点击查看代码
#define int long long
const int N = 3010;
int n, m, k, pl[N], vs[N], U[N], V[N], W[N];
vector<pair<int, int> > g[N];
ll ds[N];
ll dij(){
priority_queue<pair<int, int> > q;
memset(ds, 0x3f, sizeof(ds));
memset(vs, 0, sizeof(vs));
ds[1] = 0;
q.push(make_pair(0, 1));
while(!q.empty()){
int x = q.top().second;
q.pop();
if(vs[x]){
continue;
}
vs[x] = 1;
for(auto i : g[x]){
int y = i.first, z = i.second;
if(ds[y] > ds[x] + z){
ds[y] = ds[x] + z;
q.push(make_pair(-ds[y], y));
}
}
}
return ds[n];
}
void solve(){
read(n, m, k);
for(int i = 1; i <= m; ++ i){
read(U[i], V[i], W[i]);
pl[i] = W[i];
}
sort(pl + 1, pl + m + 1);
int tp = unique(pl + 1, pl + m + 1) - pl - 1;
ll ans = 1e18;
for(int i = 0; i <= tp; ++ i){
for(int j = 1; j <= m; ++ j){
g[U[j]].emplace_back(V[j], max(0ll, W[j] - pl[i]));
g[V[j]].emplace_back(U[j], max(0ll, W[j] - pl[i]));
}
ans = min(ans, dij() + (ll)k * pl[i]);
for(int j = 1; j <= n; ++ j){
vector<pair<int, int> > ().swap(g[j]);
}
}
println(ans);
}
13. CF575A - Fibonotci [*2700]
直接矩阵快速幂就行了。
点击查看代码
const int N = 5e4 + 10;
int n, m;
ll s[N], P, k;
pair<ll, ll> q[N];
map<ll, ll> mp;
struct mat{
ll a, b, c, d;
} t[N*4];
mat operator * (mat x, mat y){
mat z;
z.a = (x.a * y.a + x.b * y.c) % P;
z.b = (x.a * y.b + x.b * y.d) % P;
z.c = (x.c * y.a + x.d * y.c) % P;
z.d = (x.c * y.b + x.d * y.d) % P;
return z;
}
void bld(int p, int l, int r){
if(l == r){
t[p] = { 0, s[(l+n-1)%n], 1, s[l%n] };
} else {
int mid = l + r >> 1;
bld(p<<1, l, mid);
bld(p<<1|1, mid+1, r);
t[p] = t[p<<1] * t[p<<1|1];
}
}
mat qry(int p, int l, int r, int ql, int qr){
if(qr < l || r < ql){
return { 1, 0, 0, 1 };
} else if(ql <= l && r <= qr){
return t[p];
} else {
int mid = l + r >> 1;
return qry(p<<1, l, mid, ql, qr) * qry(p<<1|1, mid+1, r, ql, qr);
}
}
mat qp(mat x, ll y){
mat ans = x;
-- y;
while(y){
if(y & 1){
ans = ans * x;
}
x = x * x;
y >>= 1;
}
return ans;
}
void cg(mat &nw, ll l, ll r){
if(l > r){
return;
}
if(mp[l-1]){
nw = nw * (mat){ 0, mp[l-1], 1, s[l%n] };
++ l;
}
if(l > r){
return;
}
if(l / n == r / n){
nw = nw * qry(1, 0, n-1, l%n, r%n);
} else {
nw = nw * qry(1, 0, n-1, l%n, n-1);
ll L = (l / n + 1) * n, R = (r / n) * n;
if(L != R) nw = nw * qp(qry(1, 0, n-1, 0, n-1), (R-L) / n);
nw = nw * qry(1, 0, n-1, 0, r%n);
}
}
void solve(){
scanf("%lld%lld%d", &k, &P, &n);
if(k == 0){
puts("0");
return;
}
for(int i = 0; i < n; ++ i){
scanf("%lld", &s[i]);
}
scanf("%d", &m);
int tp = 0;
for(int i = 1; i <= m; ++ i){
ll x, y;
scanf("%lld%lld", &x, &y);
if(x < k){
q[++tp] = make_pair(x, y);
mp[x] = y;
}
}
m = tp;
sort(q + 1, q + m + 1);
bld(1, 0, n-1);
mat nw = { 1, 0, 0, 1 };
for(int i = 1; i < n; ++ i){
nw = nw * qry(1, 0, n-1, i%n, i%n);
if(i == k - 1){
printf("%lld\n", nw.d);
return;
}
}
ll l = n, r, vl;
for(int i = 1; i <= m; ++ i){
r = q[i].first, vl = q[i].second;
if(r == q[i-1].first) continue;
cg(nw, l, r-1);
if(mp[r-1]){
nw = nw * (mat){ 0, mp[r-1], 1, vl };
} else {
nw = nw * (mat){ 0, s[(r-1)%n], 1, vl };
}
l = r + 1;
}
cg(nw, l, k-1);
printf("%lld\n", nw.d % P);
return;
}
14. CF888G - Xor-MST [*2300]
性质图 MST 考虑 boruvka。问题转化为如何求集合去掉一个子集剩下部分与 \(x\) 的最小异或和。可以使用 trie 来维护全集,查询的时候暴力删掉子集里的元素即可。易得复杂度是正确的。
点击查看代码
const int N = 2e5 + 10;
int n, a[N], fa[N];
int ch[N*34][2], siz[N*34], tot, ed[N*34];
vector<int> son[N];
void ins(int x, int vl){
int p = 0;
for(int i = 30; i >= 0; -- i){
if(!ch[p][(a[x]>>i)&1]){
ch[p][(a[x]>>i)&1] = ++ tot;
}
p = ch[p][(a[x]>>i)&1];
siz[p] += vl;
}
ed[p] = x;
}
int fnd(int x){
int p = 0;
for(int i = 30; i >= 0; -- i){
if(siz[ch[p][(x>>i)&1]]){
p = ch[p][(x>>i)&1];
} else {
p = ch[p][1-((x>>i)&1)];
}
}
return ed[p];
}
int gf(int x){
return x == fa[x] ? x : gf(fa[x]);
}
void mg(int x, int y){
x = gf(x);
y = gf(y);
if(son[x].size() < son[y].size()){
swap(x, y);
}
fa[y] = x;
for(int i : son[y]){
son[x].push_back(i);
}
}
void solve(){
read(n);
for(int i = 1; i <= n; ++ i){
read(a[i]);
}
sort(a + 1, a + n + 1);
n = unique(a + 1, a + n + 1) - a - 1;
for(int i = 1; i <= n; ++ i){
fa[i] = i;
ins(i, 1);
son[i].push_back(i);
}
int cnt = n;
ll ans = 0;
while(cnt > 1){
static int gx[N], gy[N];
for(int j = 1; j <= n; ++ j){
if(j == gf(j)){
int vl = 2e9;
for(int k : son[j]){
ins(k, -1);
}
for(int k : son[j]){
int p = fnd(a[k]);
if(vl > (a[p] ^ a[k])){
vl = a[p] ^ a[k];
gx[j] = k;
gy[j] = p;
}
}
for(int k : son[j]){
ins(k, 1);
}
}
}
for(int j = 1; j <= n; ++ j){
if(gf(j) == j && gf(gx[j]) != gf(gy[j])){
mg(gx[j], gy[j]);
ans += a[gx[j]] ^ a[gy[j]];
-- cnt;
}
}
}
println(ans);
}
15. Dynamic Rankings [省选/NOI-]
树状数组套线段树。
树状数组每个节点维护的是一段区间的值,那么我们可以用线段树来维护这段区间。单点加就在树状数组上若干点对应线段树加,区间查就提取出区间,在维护这些区间的线段树上一起查。
点击查看代码
const int N = 2e5 + 10, M = 4e7 + 10;
int n, m, a[N], b[N], tp, rt[N];
struct qry{
int op, l, r, k;
} q[N];
int t[M], cnt, ls[M], rs[M];
vector<int> ts, qs;
void add(int &p, int l, int r, int x, int v){
++ cnt;
t[cnt] = t[p];
ls[cnt] = ls[p];
rs[cnt] = rs[p];
p = cnt;
if(l == r){
t[p] += v;
} else {
int mid = l + r >> 1;
if(x <= mid){
add(ls[p], l, mid, x, v);
} else {
add(rs[p], mid+1, r, x, v);
}
t[p] = t[ls[p]] + t[rs[p]];
}
}
int qry(int l, int r, int k){
if(l == r){
return l;
}
int mid = l + r >> 1, x = 0, p = ts.size();
for(int i = 0; i < p; ++ i){
x += qs[i] * t[ls[ts[i]]];
}
if(x >= k){
for(int i = 0; i < p; ++ i){
ts[i] = ls[ts[i]];
}
return qry(l, mid, k);
} else {
for(int i = 0; i < p; ++ i){
ts[i] = rs[ts[i]];
}
return qry(mid+1, r, k-x);
}
}
void Add(int x, int v, int op){
while(x <= n){
add(rt[x], 1, tp, v, op);
x += x & -x;
}
}
int Qry(int l, int r, int k){
vector<int> ().swap(ts);
vector<int> ().swap(qs);
while(r){
ts.push_back(rt[r]);
qs.push_back(1);
r -= r & -r;
}
-- l;
while(l){
ts.push_back(rt[l]);
qs.push_back(-1);
l -= l & -l;
}
return qry(1, tp, k);
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
b[i] = a[i];
}
tp = n;
for(int i = 1; i <= m; ++ i){
char s[4];
scanf("%s", s);
if(s[0] == 'Q'){
q[i].op = 0;
scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].k);
} else {
q[i].op = 1;
scanf("%d%d", &q[i].l, &q[i].r);
b[++tp] = q[i].r;
}
}
sort(b + 1, b + tp + 1);
tp = unique(b + 1, b + tp + 1) - b - 1;
for(int i = 1; i <= n; ++ i){
a[i] = lower_bound(b + 1, b + tp + 1, a[i]) - b;
Add(i, a[i], 1);
}
for(int i = 1; i <= m; ++ i){
if(q[i].op == 1){
Add(q[i].l, a[q[i].l], -1);
a[q[i].l] = lower_bound(b + 1, b + tp + 1, q[i].r) - b;
Add(q[i].l, a[q[i].l], 1);
} else {
printf("%d\n", b[Qry(q[i].l, q[i].r, q[i].k)]);
}
}
return 0;
}
16. EC Online 2023 (I) - Alice and Bob
考场策略肯定是打表找规律,可以发现如下规律:
三元组 \((i,j,k)(i\leq j\leq k)\) 必败,当且仅当 \(i==j\) 且 \(k-j\) 的质因数分解中有偶数个 \(2\)。
知道这个结论后就很好做了。把每个数倒着插入 trie 中即可。
点击查看代码
int T, n, cnt[N], ps[N];
ll a[N], b[N];
inline ll clc(ll x){
return x * (x-1) * (x-2) / 6;
}
int ch[N*64][2], siz[N*64], tt;
inline void ins(ll x){
int p = 0;
++ siz[p];
for(ll i = 0; i <= 62; ++ i){
if(!ch[p][(x>>i)&1]){
ch[p][(x>>i)&1] = ++ tt;
}
p = ch[p][(x>>i)&1];
++ siz[p];
}
}
inline void qry(ll x, ll &ans){
int p = 0, op = 1;
ans += siz[p] * op;
op *= -1;
for(ll i = 0; i <= 62; ++ i){
p = ch[p][(x>>i)&1];
ans += siz[p] * op;
op *= -1;
}
}
int main(){
scanf("%d", &T);
while(T--){
scanf("%d", &n);
ll ans = clc(n);
for(int i = 1; i <= n; ++ i){
scanf("%lld", &a[i]);
b[i] = a[i];
ins(a[i]);
}
sort(b + 1, b + n + 1);
int m = unique(b + 1, b + n + 1) - b - 1;
for(register int i = 1; i <= n; ++ i){
ps[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
++ cnt[ps[i]];
}
for(register int i = 1; i <= n; ++ i){
int p = ps[i];
if(cnt[p]){
ll cc = cnt[p], dd = 0;
cnt[p] = 0;
qry(a[i], dd);
ans -= dd * cc * (cc-1) / 2 + clc(cc);
}
}
printf("%lld\n", ans);
for(register int i = 0; i <= tt; ++ i){
ch[i][0] = ch[i][1] = siz[i] = 0;
}
for(register int i = 0; i <= n; ++ i){
cnt[i] = 0;
}
tt = 0;
}
return 0;
}
17. CF1615F - LEGOndary Grandmaster [*2800]
将两个串的奇数位取反,发现操作变为了交换相邻位的值。那么我们就可以 dp:设 \(f_{i,j}\) 表示前 \(i\) 位,第一个串还有 \(j\) 位没有匹配的方案数;\(g_{i,j}\) 表示同样状态的贡献和。\(f\) 的转移显然,\(g\) 的转移:\(g_{i,k}=f_{i-1,j}*|j|+g_{i-1,j}\)。
点击查看代码
const int N = 2010;
int T, n;
char s[N], t[N];
const ll P = 1e9 + 7;
ll f[N][N*2], g[N][N*2];
int main(){
scanf("%d", &T);
while(T--){
scanf("%d%s%s", &n, s + 1, t + 1);
for(int i = 1; i <= n; ++ i){
memset(f[i], 0, sizeof(f[i]));
memset(g[i], 0, sizeof(g[i]));
if(s[i] != '?' && (i & 1)){
s[i] = '0' + '1' - s[i];
}
if(t[i] != '?' && (i & 1)){
t[i] = '0' + '1' - t[i];
}
}
f[0][n] = 1;
for(int i = 1; i <= n; ++ i){
for(int j = 0; j <= n+n; ++ j){
for(int k = 0; k < 2; ++ k){
for(int l = 0; l < 2; ++ l){
if(s[i] != '?' && s[i] != k + '0'){
continue;
}
if(t[i] != '?' && t[i] != l + '0'){
continue;
}
if(j+k-l < 0){
continue;
}
f[i][j+k-l] = (f[i][j+k-l] + f[i-1][j]) % P;
g[i][j+k-l] = (g[i][j+k-l] + g[i-1][j] + f[i-1][j] * abs(j-n)) % P;
}
}
}
}
printf("%lld\n", g[n][n]);
f[0][n] = 0;
}
return 0;
}
18. 湖北省选模拟 2023 - 棋圣 / alphago [省选/NOI-]
考虑答案上界:
- 若原图不是二分图,那么每两个棋子之间都可能距离 \(1\)。上界是 \(cnt_0\times cnt_1\times \max_{(u,v)\in E}w_{u,v}\)。
- 否则,只有染色不同的两个棋子之间可能距离 \(1\)。上界是 \((cnt_{0,1}\times cnt_{1,0} + cnt{0,0}\times cnt_{1,1}) \max_{(u,v)\in E}w_{u,v}\)。
可以证明只有原图是链的情况达不到上界。
proof
若图中存在环,首先可以将所有棋子移动到环上。环上的所有棋子都可以任意选择一个方向移动一步,就可以将所有棋子聚在一起。
若不存在环且不是链,则存在一个度数 \(\geq 3\) 的点。反证,若存在三个棋子使得其中任意两个永远不可能在同一个位置,则我们可以将他们移动到这个点以及它的两个邻点上,接着再次操作一个没有棋子的邻点,使得其中两个在同一个位置,矛盾。
最后是链的情况,发现两个棋子可行的距离与原距离同奇偶且不大于原距离。于是可以设 \(f_{i,j,k}\) 表示 \([i,j]\) 的棋子在第 \(k\) 个点上的最大答案,dp 即可。
点击查看代码
const int N = 110;
int n, m, k, cl[N], mx, mxdeg;
pair<int, int> ch[N];
vector<pair<int, int> > g[N];
void dfs(int x, int c){
cl[x] = c;
for(auto i : g[x]){
if(cl[i.first] == -1){
dfs(i.first, 1-c);
}
}
}
int main(){
scanf("%d%d%d", &n, &m, &k);
for(int i = 1; i <= k; ++ i){
scanf("%d%d", &ch[i].first, &ch[i].second);
}
for(int i = 1; i <= m; ++ i){
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
mx = max(mx, w);
g[u].emplace_back(v, w);
g[v].emplace_back(u, w);
}
for(int i = 1; i <= n; ++ i){
mxdeg = max(mxdeg, (int)g[i].size());
}
memset(cl, -1, sizeof(cl));
dfs(1, 0);
if(mxdeg > 2 || m >= n){
bool flg = 1;
for(int i = 1; i <= n; ++ i){
for(auto j : g[i]){
if(cl[i] == cl[j.first]){
flg = 0;
break;
}
}
}
if(flg){
ll x = 0, xx = 0, y = 0, yy = 0;
for(int i = 1; i <= k; ++ i){
if(ch[i].second == 0){
if(cl[ch[i].first]){
++ x;
} else {
++ xx;
}
} else {
if(cl[ch[i].first]){
++ y;
} else {
++ yy;
}
}
}
printf("%lld\n", (x * yy + xx * y) * mx);
} else {
ll x = 0, y = 0;
for(int i = 1; i <= k; ++ i){
if(ch[i].second == 0){
++ x;
} else {
++ y;
}
}
printf("%lld\n", x * y * mx);
}
} else {
static int nid[N], w[N], sum[N][2], ok[N][N];
memset(ok, 0, sizeof(ok));
memset(sum, 0, sizeof(sum));
int x = 0, ls = 0;
for(int i = 1; i <= n; ++ i){
if(g[i].size() == 1){
x = i;
}
}
nid[x] = 1;
for(int i = 2; i <= n; ++ i){
for(auto j : g[x]){
if(j.first != ls){
nid[j.first] = i;
w[i-1] = j.second;
ls = x;
x = j.first;
break;
}
}
}
for(int i = 1; i <= k; ++ i){
ch[i].first = nid[ch[i].first];
}
sort(ch + 1, ch + k + 1);
for(int i = 1; i <= k; ++ i){
for(int j = i; j <= k; ++ j){
int flg = 1;
for(int k = i; k <= j; ++ k){
if((ch[i].first + ch[k].first) & 1){
flg = 0;
break;
}
}
ok[i][j] = flg;
}
}
for(int i = 1; i <= k; ++ i){
++ sum[i][ch[i].second];
sum[i][0] += sum[i-1][0];
sum[i][1] += sum[i-1][1];
}
static int f[N][N][N];
for(int i = 1; i <= k; ++ i){
for(int j = 1; j <= i; ++ j){
if(!ok[j][i]){
continue;
}
for(int p = 1; p <= n; ++ p){
if(i == 1){
f[j][i][p] = 0;
}
int r = j - 1;
for(int l = 1; l <= r; ++ l){
if(!ok[l][r]){
continue;
}
for(int q = 1; q < p; ++ q){
if((p + q + ch[r].first + ch[j].first) & 1){
continue;
}
if(p - q > ch[j].first - ch[r].first){
continue;
}
int val = 0;
if(q + 1 == p){
val += (sum[i][1] - sum[j-1][1]) * (sum[r][0] - sum[l-1][0]);
val += (sum[i][0] - sum[j-1][0]) * (sum[r][1] - sum[l-1][1]);
}
f[j][i][p] = max(f[j][i][p], f[l][r][q] + val * w[q]);
}
}
}
}
}
int ans = 0;
for(int i = 1; i <= k; ++ i){
for(int j = 1; j <= k; ++ j){
for(int p = 1; p <= n; ++ p){
ans = max(ans, f[i][j][p]);
}
}
}
printf("%d\n", ans);
}
return 0;
}
19. AT_abc341_g - Highest Ratio [*2208]
\(\dfrac 1{r-k+1}\sum\limits_{i=k}^r A_i=\dfrac{S_r-S_{k-1}}{r-(k-1)}\)。
所以从右往左维护 \((i,S_i)\) 构成的上凸壳即可。
点击查看代码
const int N = 2e5 + 10;
int n, st[N], tp, r[N];
typedef long long ll;
ll a[N], sum[N];
bool calc(int x, int y, int z){
return (sum[x] - sum[z]) * (y - z) >= (sum[y] - sum[z]) * (x - z);
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%lld", &a[i]);
sum[i] = sum[i-1] + a[i];
}
st[++tp] = n;
for(int i = n-1; i >= 0; -- i){
while(tp >= 2 && calc(st[tp-1], st[tp], i)){
-- tp;
}
r[i+1] = st[tp];
st[++tp] = i;
}
for(int i = 1; i <= n; ++ i){
printf("%.8lf\n", (sum[r[i]] - sum[i-1]) * 1.0 / (r[i] - i + 1));
}
return 0;
}
20. 湖北省选模拟 2023 - 环山危路 / road [省选/NOI-]
竞赛图最大流。边数真的很多。
考虑转化为最小割,设割完后起点所在点集为 \(S\),终点所在点集为 \(T\),\(f(S,T)\) 为 \(S,T\) 的割集使 \(T\) 中点不能到 \(S\)。显然有 \(f(S,T)-f(T,S)=\sum_{i\in S}deg_{i,out} - deg_{i,in}\)。
又因为竞赛图性质有 \(f(S,T)+f(T,S)=|S|\times |T|\)。所以 \(f(S,T)=\dfrac{|S|\times |T|+\sum_{i\in S}deg_{i,out} - deg_{i,in}}2\)。所以枚举 \(|S|\) 然后贪心地选点即可。
点击查看代码
const int N = 3010;
int n, m, ind[N], oud[N], p[N];
char e[N][N];
bool cmp(int x, int y){
return oud[x] - ind[x] < oud[y] - ind[y];
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++ i){
scanf("%s", e[i] + 1);
for(int j = 1; j <= n; ++ j){
if(e[i][j] == '1'){
++ ind[j];
++ oud[i];
}
}
p[i] = i;
}
sort(p + 1, p + n + 1, cmp);
while(m--){
int ed, k, S = 0, T = n, sum = 0, ans = 0;
static int s[N], is[N];
memset(is, 0, sizeof(is));
scanf("%d%d", &ed, &k);
for(int i = 1; i <= k; ++ i){
scanf("%d", &s[i]);
is[s[i]] = 1;
++ S;
-- T;
sum += oud[s[i]] - ind[s[i]];
}
ans = S * T + sum;
for(int i = 1; i <= n; ++ i){
if(is[p[i]] || p[i] == ed){
continue;
}
++ S;
-- T;
sum += oud[p[i]] - ind[p[i]];
ans = min(ans, S * T + sum);
}
printf("%d\n", ans / 2);
}
return 0;
}
21. CF568B - Symmetric and Transitive [*1900]
为什么会有不满足自反性但满足传递性与对称性的二元关系呢?重点在于我们可能没办法对于每个 \(a\) 找到 \(b\) 使得 \(\rho(a,b)\),即把二元关系写成边,构成若干个团和至少一个孤立点(一个点的自环不算孤立点)。
容易发现没有孤立点的话方案数是贝尔数 \(B_n\)。link。有孤立点我们枚举非孤立点数量得 \(ans=\sum_{i=0}^{n-1}\dbinom{n}{i} B_i=B_{n+1}-B_n\)。
点击查看代码
const int N = 4010;
const ll P = 1e9 + 7;
int n;
ll bell[N][N];
void solve(){
read(n);
bell[0][0] = 1;
for(int i = 1; i <= n; ++ i){
bell[i][0] = bell[i-1][i-1];
for(int j = 1; j <= i; ++ j){
bell[i][j] = (bell[i-1][j-1] + bell[i][j-1]) % P;
}
}
println(bell[n][n-1]);
}
22. ARC162F - Montage [*3190]
*3190? *2190!
首先可以钦定 \(i\) 行 \(j\) 列有 \(1\),其他列没有 \(1\),最后算出方案后乘上组合数更新答案。根据题目,若一个矩形左上、右下两个位置是 \(1\),则四个角都要是 \(1\),所以我们可以发现合法的方案是左下到右上的两个递增阶梯 \(a,b\),\(b\) 中所有位置都在 \(a\),中,再把在 \(a\) 中不在 \(b\) 中的位置设为 \(1\)。
那么就可以 dp:设 \(f_{i,l,r}\) 表示第 \(i\) 列,\([l,r]\) 行为 \(1\) 的方案数,发现可以转移到 \(f_{i,l,r}\) 的 \(f_{i-1,p,q}\) 需满足 \(p\leq l,\max(1,l-1)\leq q\leq r\),是一个矩形,可以二维前缀和优化。
点击查看代码
const int N = 410;
const ll P = 998244353;
int n, m;
ll C[N][N], f[N][N], g[N][N];
ll sum[N][N];
int main(){
scanf("%d%d", &n, &m);
C[0][0] = 1;
if(n < m){
swap(n, m);
}
for(int i = 1; i <= n; ++ i){
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i; ++ j){
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
}
}
for(int i = 1; i <= m; ++ i){
f[1][i] = g[1][i] = 1;
}
for(int l = 1; l <= m; ++ l){
for(int r = 1; r <= m; ++ r){
sum[l][r] = sum[l-1][r] + sum[l][r-1] - sum[l-1][r-1] + f[l][r] + P;
while(sum[l][r] >= P) sum[l][r] -= P;
}
}
for(int i = 2; i <= n; ++ i){
for(int l = 1; l <= m; ++ l){
for(int r = l; r <= m; ++ r){
f[l][r] = sum[l][r] - sum[l][max(0, l-2)] + P;
while(f[l][r] >= P) f[l][r] -= P;
}
}
for(int l = 1; l <= m; ++ l){
for(int r = 1; r <= m; ++ r){
sum[l][r] = sum[l-1][r] + sum[l][r-1] - sum[l-1][r-1] + f[l][r] + P;
while(sum[l][r] >= P) sum[l][r] -= P;
}
}
for(int j = 1; j <= m; ++ j){
for(int p = 1; p <= j; ++ p){
g[i][j] = g[i][j] + f[p][j];
while(g[i][j] >= P) g[i][j] -= P;
}
}
}
ll ans = 1;
for(int i = 1; i <= n; ++ i){
for(int j = 1; j <= m; ++ j){
ans = (ans + C[n][i] * C[m][j] % P * g[i][j]) % P;
}
}
printf("%lld\n", ans);
return 0;
}
23. AGC046F - Forbidden Tournament [*3854]
引理 1. 竞赛图缩点后形成一条链。
引理 2. 竞赛图的任意一个点数 \(\geq 3\) 的 SCC 中存在三元环 \((u,v)(v,w)(w,u)\)。
两个引理都是竞赛图的基本性质。
对这个竞赛图进行缩点,设链上的 SCC 点集依次为 \(s_1,s_2,...,s_p\),则若存在 \(i\) 使 \(i<p,|s_i|\ge 3\),则选择一个属于该 SCC 的三元环 \((u,v,w)\) 以及 \(s_p\) 中的一个点 \(z\),\((u,v,w,z)\) 构成一个子图。所以 \(\forall i<p,|s_i|<3\);又因为不存在大小为 \(2\) 的 SCC,所以 \(\forall i<p,|s_i|=1\)。
枚举 \(p\),问题转化为求大小为 \(N-p-1\)、不存在给定子图、每个点入度 \(\leq K-p-1\) 的 SCC 的数量。设 \(n=N-p-1,m=K-p-1\)。
考虑任选 SCC 中一点 \(x\),则其余 \(n-1\) 个点可以被分为两个集合 \(S,T\),其中 \(S\) 中点都有连向 \(x\) 的边,\(x\) 所有连出去的边都连向 \(T\) 中的点。由于 \(x\) 在 SCC 中,所以 \(|S|,|T|\geq 1\)。
容易发现 \(S\) 一定构成一条链。否则 \(S\) 中会存在大小 \(\geq 3\) 的 SCC,其中取出一个三元环,这三个点与 \(x\) 会构成一个子图。
\(T\) 也一定构成一条链。否则 \(T\) 中会存在大小 \(\geq 3\) 的 SCC,其中取出三元环 \((u,v,w)\)。设 \(S\) 链的第一个节点为 \(s\),\(T\) 的子集 \(T_1\) 中的点连向了 \(s\),\(s\) 连向了 \(T\) 的子集 \(T_2\) 中的点。若 \(|T_2|=0\),则 \(u,v,w\in T_1\),\((u,v,w,s)\) 构成子图;若 \(|T_1|=0\),则整个图构成不了 SCC;若存在 \(a\in T_1,b\in T_2\) 使得存在 \((a,b)\),则 \((a,s,x,b)\) 构成子图;否则不存在 \(T_2\) 向 \(T_1\) 连的边,图构成不了 SCC。
把两条链上的节点按照拓扑序重标号。可以发现对于 \(s_i\in S\),其连向 \(T\) 的节点一定是一个前缀。否则存在 \(t_j,t_{j+1}\in T\) 存在边 \((t_j,s_i),(s_i,t_{j+1})\),\((s_i,t_j,x,t_{j+1})\) 构成子图。设 \(p_i\) 表示 \(\forall j\leq p_i\) 存在边 \((s_i,t_j)\) 且不存在 \((s_i,t_{j+1})\)。
可以发现 \(p_i\leq p_{i+1},p_1< |T|\):
若 \(p_1=|T|\),则图构成不了 SCC。
若 \(p_i>p_{i+1}\):
- 若 \(p_i<|T|\),则 \((t_{p_i},t_{p_{i+1}},s_i,s_{i+1})\) 构成子图。
- 若 \(p_i=|T|\),则 \((t_{|T|},s_1,s_i,s_{i+1})\) 构成子图。
所以就可以 dp。设 \(f_{i,j}\) 表示左边第 \(i\) 个点的 \(p_i=j\) 的方案数。枚举左边点数 \(p\),则右边点数为 \(n-p-1\)。转移时用 \(f_{i-1,k}\) 更新 \(f_{i,j}(k\leq j)\)。
注意一个状态 \(f_{i,j}\) 可行当且仅当:
- \(i\neq 1\) 或 \(j\neq q\)。
- \((q-j)+(i-1) \leq m\),即 \(s_i\) 的入度 \(\leq m\)。
- \((p-i+1)+1+(j-1) \leq m\),即 \(t_j\) 的入度 \(\leq m\)。
最后设 \(sum=\sum_{i=0}^q f_{p,i}\),即为左边 \(p\) 个点右边 \(q\) 个点的方案数。对答案的贡献要乘上 \(\dbinom{n-1}pp!q!\),因为两边点可以随便排列,还要选择每个点到底在哪一边。注意这里是选择了中间点 \(x\),所以 \(x\) 是唯一的,不用管。
现在求出 \(ans\) 为 \(n=N-p-1,m=K-p-1\) 的答案,对总答案的贡献要乘上 \(\dbinom npp!\),表示选择前面链上的若干个点。
点击查看代码
const int N = 210;
ll P, C[N][N], fac[N], ans, f[N][N];
int n, m;
ll calc(int n, int m){
ll res = 0;
for(int p = 1; p < n - 1; ++ p){
int q = n - 1 - p;
ll sum = 0;
f[1][q] = 0;
if(p > m || q > m){
continue;
}
for(int i = 0; i < q; ++ i){
f[1][i] = 1;
if(q-i > m || (i && p+i > m)){
f[1][i] = 0;
}
}
for(int i = 2; i <= p; ++ i){
ll pr = 0;
for(int j = 0; j <= q; ++ j){
pr = (pr + f[i-1][j]) % P;
f[i][j] = pr;
if(i-1+q-j > m || (j && p-i+1+j > m)){
f[i][j] = 0;
}
}
}
for(int i = 0; i <= q; ++ i){
sum = (sum + f[p][i]) % P;
}
res = (res + C[n-1][p] * fac[p] % P * fac[q] % P * sum) % P;
}
return res;
}
int main(){
scanf("%d%d%lld", &n, &m, &P);
fac[0] = C[0][0] = 1;
for(int i = 1; i <= n; ++ i){
fac[i] = fac[i-1] * i % P;
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i; ++ j){
C[i][j] = (C[i-1][j-1] + C[i-1][j]) % P;
}
}
if(m == n - 1){
ans = fac[n];
}
for(int i = 0; i <= min(m, n - 3); ++ i){
ans = (ans + fac[i] * C[n][i] % P * calc(n-i, m-i)) % P;
}
printf("%lld\n", ans);
return 0;
}
24. BJOI2014 - 大融合 [省选/NOI-]
首先离线,把树建出来。若不连通在最后补边使得连通。
然后遍历操作:
-
若为添加操作 \((x,y)\),不妨 \(x\) 是 \(y\) 的父亲,则使用并查集维护 \(x\) 到最浅的一个节点 \(tp\) 使得 \(x\to tp\) 的路径都是添加过的。然后用树剖将这些点的 \(siz\) 都加上 \(siz_y\)。
-
若为查询操作 \((x,y)\),不妨 \(x\) 是 \(y\) 的父亲,则两部分大小分别为 \(siz_{tp_x}-siz_y,siz_y\)。
点击查看代码
const int N = 2e5 + 10;
int n, q, fa[N], dep[N], siz[N], anc[N], sz[N], son[N], dfn[N], top[N];
tuple<int, int, int> op[N];
vector<int> g[N];
int gf(int x){
return x == fa[x] ? x : fa[x] = gf(fa[x]);
}
void dfs(int x, int fat){
sz[x] = 1;
dep[x] = dep[fat] + 1;
anc[x] = fat;
for(int i : g[x]){
if(i == fat){
continue;
}
dfs(i, x);
sz[x] += sz[i];
if(sz[i] > sz[son[x]]){
son[x] = i;
}
}
}
void dfss(int x, int tp){
dfn[x] = ++ *dfn;
top[x] = tp;
if(son[x]){
dfss(son[x], tp);
}
for(int i : g[x]){
if(i != anc[x] && i != son[x]){
dfss(i, i);
}
}
}
ll t[N*4], tag[N*4];
void psd(int p, int l, int r){
int mid = l + r >> 1;
t[p<<1] += tag[p] * (mid - l + 1);
t[p<<1|1] += tag[p] * (r - mid);
tag[p<<1] += tag[p];
tag[p<<1|1] += tag[p];
tag[p] = 0;
}
void add(int p, int l, int r, int ql, int qr, ll v){
if(qr < l || r < ql){
return;
} else if(ql <= l && r <= qr){
t[p] += v * (r - l + 1);
tag[p] += v;
} else {
int mid = l + r >> 1;
psd(p, l, r);
add(p<<1, l, mid, ql, qr, v);
add(p<<1|1, mid+1, r, ql, qr, v);
t[p] = t[p<<1] + t[p<<1|1];
}
}
ll qry(int p, int l, int r, int x){
if(l == r){
return t[p];
} else {
int mid = l + r >> 1;
psd(p, l, r);
if(x <= mid){
return qry(p<<1, l, mid, x);
} else {
return qry(p<<1|1, mid+1, r, x);
}
}
}
void add(int x, int y, ll v){
while(top[x] != top[y]){
if(dep[top[x]] < dep[top[y]]){
swap(x, y);
}
add(1, 1, n, dfn[top[x]], dfn[x], v);
x = anc[top[x]];
}
if(dep[x] > dep[y]){
swap(x, y);
}
add(1, 1, n, dfn[x], dfn[y], v);
}
int main(){
scanf("%d%d", &n, &q);
for(int i = 1; i <= n; ++ i){
fa[i] = i;
}
for(int i = 1; i <= q; ++ i){
char ch[5];
int x, y;
scanf("%s%d%d", ch, &x, &y);
op[i] = { (ch[0] == 'A' ? 0 : 1), x, y };
if(ch[0] == 'A'){
g[x].push_back(y);
g[y].push_back(x);
fa[gf(x)] = gf(y);
}
}
for(int i = 1; i <= n; ++ i){
if(i == gf(i) && gf(i) != gf(1)){
op[++q] = { 0, 1, i };
g[1].push_back(i);
g[i].push_back(1);
}
}
dfs(1, 0);
dfss(1, 1);
for(int i = 1; i <= n; ++ i){
fa[i] = i;
}
add(1, 1, n, 1, n, 1);
for(int i = 1; i <= q; ++ i){
int x = get<1>(op[i]), y = get<2>(op[i]);
if(dep[x] > dep[y]){
swap(x, y);
}
if(get<0>(op[i]) == 0){
int tp = gf(x);
int sy = qry(1, 1, n, dfn[y]);
add(tp, x, sy);
fa[y] = tp;
} else {
int tp = gf(x);
int all = qry(1, 1, n, dfn[tp]);
int sy = qry(1, 1, n, dfn[y]);
printf("%lld\n", (ll)(all - sy) * sy);
}
}
return 0;
}
25. ARC162E - Strange Constraints [*2780]
考虑 dp 顺序:按数出现在 \(b\) 数组中的次数从大到小 dp。
设 \(f_{i,j,k}\) 表示 \(cnt\geq i\),填了 \(j\) 种数,\(k\) 个位置的方案数。\(pr_i\) 表示 \(\sum[a_j\geq i]\),则出现次数 \(\geq i\) 的数只能填在 \(pr_i\) 个位置上且只有 \(pr_i\) 个数出现次数可以 \(\geq i\)。
发现随着 \(i\) 的减小,可填的区间逐步增加且完全包含更大的 \(i\) 可填的区间。所以 dp 方程:
\(f_{i,j+x,k+ix}=\sum_{x}f_{i+1,j,k}\times\dbinom{pr_i-j}{x}\dfrac{(pr_i-k)!}{(pr_i-k-ix)![(i)!]^k}\)。
其中左边组合数表示从 \(pr_i\) 个可行数去掉已经选了的 \(j\) 个数中选择 \(x\) 个数。右边分式表示从 \(pr_i-k\) 个位置填上 \(ix\) 个数,且这些数分为 \(i\) 个集合,每个集合内无序的可重集方案数。
分析复杂度:看似是 \(O(n^4)\),其实 \(j,x\leq \dfrac ni\)。
所以总共的循环次数大约为 \(\sum_{i=1}^n\dfrac{n^3}{i^2}=O(n^3)\)。
点击查看代码
const int N = 510;
const ll P = 998244353;
int n, a[N], pr[N];
ll C[N][N], fac[N], inv[N], f[N][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;
}
void solve(){
read(n);
for(int i = 1; i <= n; ++ i){
read(a[i]);
}
fac[0] = C[0][0] = inv[0] = 1;
for(int i = 1; i <= n; ++ i){
fac[i] = fac[i-1] * i % P;
inv[i] = qp(fac[i], P-2);
C[i][0] = C[i][i] = 1;
for(int j = 1; j < i; ++ j){
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
}
for(int j = 1; j <= n; ++ j){
if(a[j] >= i){
++ pr[i];
}
}
}
f[n+1][0][0] = 1;
for(int i = n; i >= 1; -- i){
for(int j = 0; i * j <= n && j <= pr[i]; ++ j){
for(int k = 0; k <= pr[i]; ++ k){
if(!f[i+1][j][k]){
continue;
}
ll pw = 1;
for(int x = 0; k + x * i <= pr[i] && j + x <= n; ++ x){
ll fz = C[pr[i]-j][x] * fac[pr[i]-k] % P;
ll fm = pw * inv[pr[i]-k-x*i] % P;
(f[i][j+x][k+i*x] += f[i+1][j][k] * fz % P * fm) %= P;
pw = pw * inv[i] % P;
}
}
}
}
ll ans = 0;
for(int i = 0; i <= n; ++ i){
(ans += f[1][i][n]) %= P;
}
println(ans);
}
26. ARC112F - Die Siedler [*3432]
观察到,若可以进行操作 \(1\),那么一次操作 \(1\) 一定是更优的。所以我们先把原状态退回到全是第 \(1\) 种牌的情况,则这个状态只进行操作 1 到达的最优情况只和第 \(1\) 种牌的数量有关。
一个状态对应的第 \(1\) 种牌的数量为:\(\sum_{i=1}^nc_i\times2^i\times i!\)。设初始状态的数量为 \(st\),对应每个牌堆的状态为 \(vl_i\)。则我们每次可以让 \(st\) 加上任意一个 \(vl_i\);又因为每 \(2^n\times n!\) 个第 \(1\) 种牌又可以合成 \(1\) 个第 \(1\) 种牌,所以每次还可以让 \(st\) 减去 \(2^n\times n!-1\)。
所以最终可以到达的状态 \(ed\) 可以表示为 \(st+\sum_{i=1}^nx_ic_i-y(2^nn!-1)\)。其中 \(x_i,y\) 为任意自然数。由裴蜀定理,\(ed\) 可以到达的充要条件为 \(ed\equiv st\pmod g\),其中 \(g=\gcd(\gcd_{i=1}^nvl_i,(2^nn!-1))\)。
设 \(cnt(x)\) 表示 \(x\) 张第 \(1\) 种牌经过若干次操作 \(1\) 的最小牌数。
这时候我们有两种方法:
- 暴力枚举 \(ed=st\bmod g+kg(ed>0,k\in\mathbb N)\),计算 \(cnt(ed)\) 最小值。复杂度 \(O(\dfrac{2^nn!-1}g)\)。
- 同余最短路,建 \(g\) 个点,每个点 \(x\) 表示 \(a\equiv x\pmod g\) 的最小的 \(cnt(a)\)。初始状态 \(dis_{2^ii!\bmod g}=1\),因为 \(dis_0\) 不能取到 \(0\)。连边 \(i\to (i+2^jj!)\bmod p\),边权为 \(1\),表示添加一张第 \(j\) 种牌。最后答案取 \(dis_{st\bmod g}\)。复杂度 \(O(g)\)。
总复杂度 \(O(\sqrt{2^nn!-1})\)。但是小于 \(\sqrt{2^nn!-1}\) 且为 \({2^nn!-1}\) 因数的最大值为 \(1214827\),当 \(n=12\) 时取到。所以 \(g\leq1214827\) 或者 \(g\geq\dfrac{2^nn!-1}{1214827}\)。可以过。
点击查看代码
const int P = 1214827;
int n, m, c[20], s[55][20], d[P+10];
ll vl[20], fac[20], pw[20];
ll clc(int x[]){
ll rs = 0;
for(int i = 1; i <= n; ++ i){
rs += x[i] * (1ll << i-1) * fac[i-1];
}
return rs;
}
int cnt(ll x){
int ans = 0;
for(int i = 1; i <= n; ++ i){
ans += x % (i + i);
x /= i + i;
}
return ans;
}
int main(){
scanf("%d%d", &n, &m);
fac[0] = 1;
for(int i = 1; i <= n; ++ i){
scanf("%d", &c[i]);
fac[i] = fac[i-1] * i;
}
ll st = clc(c), mx = (1ll << n) * fac[n] - 1;
ll g = mx;
for(int i = 1; i <= m; ++ i){
for(int j = 1; j <= n; ++ j){
scanf("%d", &s[i][j]);
}
vl[i] = clc(s[i]);
g = __gcd(g, vl[i]);
}
if(g >= P){
int ans = 1e9;
ll m = st % g == 0 ? g : st % g;
for(ll i = m; i <= mx; i += g){
ans = min(ans, cnt(i));
}
printf("%d\n", ans);
} else {
memset(d, -1, sizeof(d));
queue<int> q;
for(int i = 0; i <= n; ++ i){
d[fac[i]*(1ll<<i)%g] = 1;
q.push(fac[i]*(1ll<<i)%g);
}
while(q.size()){
int x = q.front();
q.pop();
for(int i = 0; i <= n; ++ i){
int y = (x + fac[i] * (1ll << i)) % g;
if(d[y] == -1){
d[y] = d[x] + 1;
q.push(y);
}
}
}
printf("%d\n", d[st%g]);
}
return 0;
}
27. ARC112E - Cigar Box [*2659]
考虑移动过的数集合 \(S\)。那么目标数列中 \(S\) 中的数的位置一定为一个前缀和一个后缀,其它的部分是一个上升的序列。
所以枚举 \(L,R\) 表示前缀、后缀的长度,如果 \([L+1,n-R]\) 是上升的则统计答案;否则答案为 \(0\)。
我们将 \(m\) 次操作分配给这 \(L+R\) 个数,方案数是 \({m \brace {L+R}}\binom{L+R}{L}\)。然后这些操作中只有每个数的最后一次有方案限制,其它两个方向都可以,乘上 \(2^{m-L-R}\)。
点击查看代码
const int N = 3010;
typedef long long ll;
const ll P = 998244353;
int n, m, a[N], pr[N];
ll C[N][N], S[N][N], pw[N];
int main(){
scanf("%d%d", &n, &m);
a[0] = n+1;
C[0][0] = S[0][0] = pw[0] = 1;
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
if(a[i] > a[i-1]){
pr[i] = pr[i-1];
} else {
pr[i] = i;
}
}
for(int i = 1; i <= m; ++ i){
C[i][0] = 1;
S[i][0] = 0;
pw[i] = pw[i-1] * 2 % P;
for(int j = 1; j <= i; ++ j){
S[i][j] = (S[i-1][j-1] + S[i-1][j] * j) % P;
C[i][j] = (C[i-1][j] + C[i-1][j-1]) % P;
}
}
ll ans = 0;
for(int L = 0; L <= n; ++ L){
for(int R = 0; L + R <= min(n, m); ++ R){
if(pr[n-R] <= L + 1){
ans = (ans + S[m][L+R] * C[L+R][R] % P * pw[m-L-R]) % P;
}
}
}
printf("%lld\n", ans);
return 0;
}
28. CF1924D - Balanced Subsequences [*2700]
题意:求所有 \(n\) 个左括号,\(m\) 个右括号,最多能匹配 \(k\) 组的括号串个数。
有一个结论是只要 \(n,m\geq k\),则这个方案数只与 \(n+m,k\) 有关。
考虑把 (
写成向量 \((1,1)\),)
写成向量 \((1,-1)\),则括号串可与 \((0,0)\to(n+m,n-m)\) 的一条折线构成双射;括号串最多 \(k\) 组匹配等价于折线最低点纵坐标为 \(k-m\)(从 \(x\) 轴所有位置向右边发一束光,光能照到的 \((1,-1)\) 向量共 \(m-k\) 个一定不能匹配,其它的都可以匹配,则得证)。
最低点纵坐标为 \(k-m\) 不好做,考虑求最低点纵坐标 \(\leq k-m\),即匹配数 \(\leq k\) 的方案数。
考虑折线与 \(x=k-m\) 的直线的最后一个交点,并把之后的部分翻转。因为 \(x=n-m\) 到 \(x=k-m\) 的距离为 \(n-k\),所以翻转过后折线终点会变为 \((n+m,2k-n-m)\)。折线总数为 \(\dbinom{n+m}{k}\)。这种折线可以与最低点纵坐标 \(\leq k-m\) 的折线构成双射。
所以答案为 \(\dbinom{n+m}k-\dbinom{n+m}{k-1}\)。
another problem
给 \(n\),对于所有 \(k\in[0,n]\),求 \(k\) 个左括号 \(n−k\) 个右括号构成的括号序列,最长合法括号子序列的长度之和。
考虑递推,设 \(f_i\) 表示 \(k=i\) 时的答案。则 \(f_i\) 为 \(i\) 个左括号构成最长合法括号子序列长度 \(<i,=i\) 的答案加起来。由于上文提到的结论,那么 \(<i\) 的答案就是 \(f_{i-1}\)!然后 \(=i\) 的部分就变成了上面的题,方案数是 \(\dbinom{n}{i}-\dbinom{n}{i-1}\),贡献是 \(i(\dbinom{n}{i}-\dbinom{n}{i-1})\)。
这个递推只适用于 \(2i\leq n+1\) 的情况。剩余的部分对称即可。
点击查看代码
const ll P = 1e9 + 7;
const int N = 5e3 + 10;
int n;
ll c, fac[N], inv[N], f[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 C (int x, int y) {
return fac[x] * inv[y] % P * inv[x-y] % P;
}
int main () {
n = 5000;
fac[0] = 1;
for (int i = 1; i <= n; ++ i) {
fac[i] = fac[i-1] * i % P;
}
inv[n] = qp (fac[n], P-2);
for (int i = n - 1; i >= 0; -- i) {
inv[i] = inv[i+1] * (i + 1) % P;
}
int t, n, m, k;
scanf("%d", &t);
while(t--){
scanf("%d%d%d", &n, &m, &k);
if(k > min(n, m)){
puts("0");
continue;
}
printf("%lld\n", (C(n+m, k) - C(n+m, k-1) + P) % P);
}
return 0;
}
29. AT_dwacon5th_prelims_e - Cyclic GCDs [*2978]
将数组从小到大排序。设 \(f_{i,j}\) 表示前 \(i\) 个数划分为 \(j\) 个圆排列的答案。容易得到递推式:
\(f_{i,j}=f_{i-1,j-1}*a_i+(i-1)*f_{i-1,j}\)。
考虑生成函数 \(G_i=\sum_{j=1}^nf_{i,j}x^j\),则有:
\(G_i=a_ixG_{i-1}+(i-1)G_{i-1}=G_{i-1}(a_ix+i-1)=\prod_{j=1}^i(a_jx+j-1)\)。
\(b_i=[x^i]G_n\),则答案为 \(\gcd_{i=1}^n[x^i]G_n\)。
有结论:设 \(c(F)\) 表示 \(\gcd_i[x^i]F\),则 \(c(FG)=c(F)c(G)\)。
proof
考虑 \(c(F)=c(G)=1\) 时,若 \(c(FG)=p\neq1\),则 \(FG\equiv0\pmod p\),又 \(F,G\not\equiv0\pmod p\),矛盾。则 \(c(FG)=1\)。
所以 \(\gcd_{i=1}^n[x^i]G_n=\prod_{i=1}^n\gcd(a_i,i-1)\)。
点击查看代码
int n, a[100010];
const ll P = 998244353;
void solve(){
int ans = 1;
read (n);
for (int i = 1; i <= n; ++ i) {
read (a[i]);
}
sort (a + 1, a + n + 1);
for (int i = 1; i <= n; ++ i) {
ans = (ll)ans * __gcd (i - 1, a[i]) % P;
}
println (ans);
}
30. JSOI2011 - 柠檬 [省选/NOI-]
可以发现对于最优方案,每个选择的区间两边端点颜色相同。所以 设 \(f_i\) 表示 \([1,i]\) 分成若干区间的最优答案。则 \(f_i\) 只会从 \(a_{j+1}=a_i\) 的 \(f_j\) 转移过来。设 \(cnt_i\) 表示位置 \(i\) 是数组中出现的第几个 \(a_i\):
\(f_i=\max_{a_{j}=a_i}\{f_{j-1}+a_i(cnt_i-cnt_j+1)^2\}\)
拆开:
\(f_i=a_icnt_i^2+\max_{a_j=a_i}\{f_{j-1}+a_jcnt_j^2+2cnt_j\times(-cnt_i-1)\}\)
可以把每个颜色分开来斜率优化。每个点的坐标为 \((2cnt_j,f_{j-1}+a_jcnt_j^2)\),考虑是上凸还是下凸/取首位还是末位:
- 由于斜率 \(-cnt_i-1\) 变小,所以维护下凸;
- 由于要取下凸最大值,所以答案取栈顶。
所以维护若干单调栈:
- 弹出栈顶若干元素并插入 \(i\);
- 弹出栈顶若干元素,使得栈顶转移最优。
点击查看代码
const int N = 1e5 + 10;
int n, ls[N];
ll f[N], a[N], cnt[N];
vector<int> g[N];
ll X(int x){
return 2 * a[x] * cnt[x];
}
ll Y(int x){
return f[x-1] + a[x] * cnt[x] * cnt[x];
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
scanf("%d", &a[i]);
vector<int> &nw = g[a[i]];
int p = nw.size();
cnt[i] = cnt[ls[a[i]]] + 1;
ls[a[i]] = i;
int k = nw.size();
while(p >= 2 &&
(Y(i)-Y(nw[p-1])) * (X(nw[p-1]) - X(nw[p-2])) >=
(Y(nw[p-1])-Y(nw[p-2])) * (X(i) - X(nw[p-1]))){
nw.pop_back();
-- p;
}
nw.push_back(i);
++ p;
while(p >= 2 &&
Y(nw[p-1]) - (cnt[i] + 1) * X(nw[p-1]) <= Y(nw[p-2]) - (cnt[i] + 1) * X(nw[p-2])){
nw.pop_back();
-- p;
}
int j = nw[p-1];
f[i] = Y(j) - (cnt[i] + 1) * X(j) + a[i] * (cnt[i] + 1) * (cnt[i] + 1);
}
printf("%lld\n", f[n]);
return 0;
}
31. 省选联考 2022 - 卡牌 [省选/NOI-]
由于值域只有 \(2000\),每个数至多包含一个 \(\geq 47\) 的质因数,考虑把质数分成 \(\leq 43\) 和 \(\geq 47\) 两部分处理。对于每个数预处理出质因数拆分 \((st,x)\),其中 \(st\) 为一个 \(14\) 位的二进制数,表示是否包含 \([2,43]\) 内的质数,\(x\) 表示这个数除掉 \(\leq 43\) 的质数后剩余的部分。然后把这些数按照 \(x=1,47,53,...\) 分成若干类,每一类分别计算后合并答案。
简单的 dp 是设 \(f_{i,j}\) 表示第 \(i\) 类中的数乘积包含状态 \(j\) 的质因数的方案数。这个很容易 dp,但是并不容易合并。由之前模拟赛爆炸的经验得这里可以先容斥,合并后再统计回来。设 \(f_{i,j}\) 表示第 \(i\) 类中的数乘积不包含状态 \(j\) 的质因数的方案数。我们可以先 dp 出 \(g_{i,j}\) 表示第 \(i\) 类中的数乘积不包含状态 \(j\) 的质因数,且包含除掉状态 \(j\) 以外的所有 \([2,43]\) 内的质因数的方案数,然后利用 FWT 即可算出 \(f_{i,j}\)。
由于 \(f_{i,j}\) 的定义,我们要合并两个状态 \(p,q\) 直接 \(f_{p,j}\times f_{q,j}\) 即可。但是还有一个细节:\(f_{i,j}\) 并没有要求要包含第 \(i\) 类表示的质因数 。所以对于一个询问的质因数集合 \(S\),若第 \(i\) 类表示的质因数在 \(S\) 中,每个 \(f_i\) 需要减一,表示去掉选择空集的方案。
容斥系数为 \((-1)^{\operatorname{popcount}(j)}\)。
点击查看代码
const int N = 2010;
const ll P = 998244353;
int n, val[N], m, p[N], pr, pid[N], qid[N], v[300];
vector<pair<int, int> > G[N];
int f[300][1<<14], g[1<<14], pw2[1000010];
int main(){
n = 1e6;
pw2[0] = 1;
for(int i = 1; i <= n; ++ i){
pw2[i] = pw2[i-1] * 2 % P;
}
scanf("%d", &n);
for(int i = 1, a; i <= n; ++ i){
scanf("%d", &a);
++ val[a];
}
pid[1] = p[1] = p[0] = 1;
for(int i = 2; i <= 2000; ++ i){
bool flg = 1;
for(int j = 2; j * j <= i; ++ j){
if(i % j == 0){
flg = 0;
break;
}
}
if(flg){
qid[i] = *p - 1;
p[++*p] = i;
if(i == 43){
pr = *p;
} else if(i > 43){
pid[i] = *p - pr + 1;
}
}
}
for(int i = 1; i <= 2000; ++ i){
int x = i, st = 0;
for(int j = 2; j <= pr; ++ j){
while(x % p[j] == 0){
x /= p[j];
st |= (1 << j-2);
}
}
if(val[i]){
G[pid[x]].emplace_back(st, val[i]);
}
}
for(int i = 1; i <= 290; ++ i){
f[i][0] = 1;
for(auto j : G[i]){
int x = j.first, v = j.second;
memcpy(g, f[i], sizeof(g));
for(int k = 0; k < (1 << 14); ++ k){
f[i][k|x] = (f[i][k|x] + (ll)g[k] * (pw2[v]-1)) % P;
}
}
for(int j = 0; j < (1 << 14); ++ j){
g[j] = f[i][(1<<14)-1-j];
}
for(int j = 0; j < (1 << 14); ++ j){
f[i][j] = g[j];
}
for(int j = 0; j < 14; ++ j){
for(int k = 0; k < (1 << 14); ++ k){
if(k & (1 << j)){
f[i][k^(1<<j)] += f[i][k];
if(f[i][k^(1<<j)] >= P){
f[i][k^(1<<j)] -= P;
}
}
}
}
}
scanf("%d", &m);
while(m--){
int c, x, st = 0;
scanf("%d", &c);
memset(v, 0, sizeof(v));
for(int i = 1; i <= c; ++ i){
scanf("%d", &x);
if(x <= 43){
st |= (1 << qid[x]);
} else {
v[pid[x]] = 1;
}
}
ll ans = 0;
for(int j = st; j >= 0; j = (j - 1) & st){
#define gg(x) (((x)&1) ? P-1 : 1)
ll tmp = gg(__builtin_popcount(j));
for(int i = 1; i <= 290; ++ i){
ll op = v[i] ? P - 1 : 0;
tmp = tmp * (f[i][j] + op) % P;
}
ans += tmp;
if(ans >= P){
ans -= P;
}
if(!j){
break;
}
}
printf("%lld\n", ans);
}
return 0;
}
32. BJWC2018 - 最长上升子序列 [省选/NOI-]
所有 LIS 长度为 \(k\) 的 \(n\) 阶排列个数等价于第一行 \(k\) 个格子的杨表数的平方。所以我们枚举杨图,并使用勾长公式计算出杨图对应的杨表数即可。复杂度 \(n^3\) 乘上 \(n\) 的拆分数,可以过。
勾长公式:\(cnt=\dfrac{n!}{\prod d_{i,j}}\)。
点击查看代码
const ll P = 998244353;
int a[30], n;
ll ans, fac = 1;
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;
}
void dfs(int x, int mx, int dep){
if(x == n){
ll mul = 1;
for(int i = 1; i <= dep; ++ i){
for(int j = 1; j <= a[i]; ++ j){
int sum = a[i] - j;
for(int k = i; k <= dep && a[k] >= j; ++ k){
++ sum;
}
mul = mul * sum % P;
}
}
ans = (ans + qp(fac * qp(mul, P-2) % P, 2) * a[1] % P) % P;
} else {
for(int j = 1; j <= mx && j + x <= n; ++ j){
a[dep+1] = j;
dfs(x+j, j, dep+1);
}
}
}
void solve(){
read(n);
for(int i = 1; i <= n; ++ i){
fac = fac * i % P;
}
dfs(0, n, 0);
println(ans * qp(fac, P-2) % P);
}
33. POI201 - Lightning Conductor [省选/NOI-]
\(a_j\leq a_i+p_i-\sqrt{|i-j|}\) 可得 \(p_i\geq a_j+\sqrt{|i-j|}-a_i\)。把 \(>j,<j\) 的 \(i\) 分开来得:
\(p_i=\max\{\max_{j<i}a_j+\sqrt{i-j}-a_i,\max_{j>i}a_j+\sqrt{j-i}-a_i\}\)。
首先只处理左半部分,设 \(f_j(x)=a_j+\sqrt{x-j}\),则每次转移要选使得 \(f_j(i)\) 最大的 \(j\) 转移到 \(i\)。
把这些函数图像画出来得:
发现最优的函数一定会是反超之前的所有函数,后来又被新出现的函数反超。
所以维护一个单调队列,其中相邻两个函数的交点横坐标递增。插入一个函数的时候求出它与队首函数的交点,然后把交点横坐标大于这个交点的队内函数出队。
转移的时候查看队首的若干个交点,把交点横坐标小于 \(i\) 的出队,最后取队首转移即可。
点击查看代码
const int N = 5e5 + 10;
int n, a[N], p[N], q[N];
double sq[N];
double fx(int p, int x){
return a[p] + sq[x-p];
}
int crs(int x, int y){
int l = y, r = n + 1;
while(l < r){
int mid = l + r >> 1;
if(fx(x, mid) > fx(y, mid)){
l = mid + 1;
} else {
r = mid;
}
}
return l;
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; ++ i){
sq[i] = sqrt(i);
scanf("%d", &a[i]);
}
int l = 1, r = 0;
for(int i = 1; i <= n; ++ i){
while(l < r && crs(q[r-1], q[r]) >= crs(q[r], i)){
-- r;
}
q[++r] = i;
while(l < r && crs(q[l], q[l+1]) <= i){
++ l;
}
p[i] = ceil(fx(q[l], i)) - a[i];
}
l = 1, r = 0;
reverse(a + 1, a + n + 1);
reverse(p + 1, p + n + 1);
for(int i = 1; i <= n; ++ i){
while(l < r && crs(q[r-1], q[r]) >= crs(q[r], i)){
-- r;
}
q[++r] = i;
while(l < r && crs(q[l], q[l+1]) <= i){
++ l;
}
p[i] = max(p[i], (int)ceil(fx(q[l], i)) - a[i]);
}
reverse(p + 1, p + n + 1);
for(int i = 1; i <= n; ++ i){
printf("%d\n", p[i]);
}
return 0;
}
34. JLOI2015 - 战争调度 [省选/NOI-]
首先有一个暴力的做法:从左往右扫所有的自根向叶的链上选择的状态 \(S\),设 \(f_{i,j,S}\) 表示前 \(i\) 个叶子,到根路径状态为 \(S\),选了 \(j\) 个战争叶子的最优解,容易发现复杂度至少是 \(O(8^n)\) 的,过不去。
还有一个做法是:dfs 遍历每个点的同时记录根到点路径的选择方案,遍历到叶子的时候根据选择方案算出叶子的答案,回溯的时候使用 dp:\(f_{x,i+j}=\max\{f_{x<<1,i}+f_{x<<1|1,j}\}\) 更新各个节点的答案。这个的复杂度是 \(\sum_{i=1}^n2^{i-1}(2^{n-i-1})^22^{i-1}+2^{n-1}2^{n-1}n=O(n4^n)\) 的,可以通过。
这两个做法看起来很相似,但是为什么复杂度不同?或者说第一个做法多计算了哪些部分?
实际上,对于根的两棵子树 \(2,3\),\(2\) 最右侧的链的状态对于 \(3\) 是完全没有影响的。统计一下发现多记录的状态和为 \(O(2^{n+1})\)。
点击查看代码
const int N = 1030;
int n, m, zz[N][13], hq[N][13], f[N][N];
void dfs(int dep, int x, int state){
if(dep == n){
int ZZ = 0, HQ = 0, p = x - (1 << n - 1) + 1;
for(int i = 1; i < n; ++ i){
if(state & (1 << i)){
ZZ += zz[p][n-i];
} else {
HQ += hq[p][n-i];
}
}
f[x][0] = HQ;
f[x][1] = ZZ;
} else {
int siz = 1 << (n - dep - 1);
for(int i = 0; i <= siz * 2; ++ i){
f[x][i] = 0;
}
dfs(dep + 1, x << 1, state);
dfs(dep + 1, x << 1 | 1, state);
for(int i = 0; i <= siz; ++ i){
for(int j = 0; j <= siz; ++ j){
f[x][i+j] = max(f[x][i+j], f[x<<1][i] + f[x<<1|1][j]);
}
}
dfs(dep + 1, x << 1, state | (1 << dep));
dfs(dep + 1, x << 1 | 1, state | (1 << dep));
for(int i = 0; i <= siz; ++ i){
for(int j = 0; j <= siz; ++ j){
f[x][i+j] = max(f[x][i+j], f[x<<1][i] + f[x<<1|1][j]);
}
}
}
}
void solve(){
read(n, m);
for(int i = 1; i <= (1 << n-1); ++ i){
for(int j = 1; j < n; ++ j){
read(zz[i][j]);
}
}
for(int i = 1; i <= (1 << n-1); ++ i){
for(int j = 1; j < n; ++ j){
read(hq[i][j]);
}
}
dfs(1, 1, 0);
int ans = 0;
for(int i = 0; i <= m; ++ i){
ans = max(ans, f[1][i]);
}
println(ans);
}