2024.02 别急记录

1. WC/CTSC2024 - 水镜 [省选/NOI-]

2L=T,我们可以发现相邻两项之间的大小关系有四种:

  • hi<hi+1
  • hi<Thi+1hi+hi+1<T
  • Thi<Thi+1hi>hi+1
  • Thi<hi+1hi+hi+1>T

那么由 1,2 可得若 hihi+1,hi+hi+1T 则一定会取 Thi

类似地总共四条:

  • hihi+1,hi+hi+1T 则一定会取 Thi
  • hihi+1,hi+hi+1T 则一定会取 Thi+1
  • hihi+1,hi+hi+1T 则一定会取 hi+1
  • hihi+1,hi+hi+1T 则一定会取 hi

那么我们接着考虑 hi1,hi,hi+1 三者的关系,可得若 hi1,hi 满足上述条件 2,hi,hi+1 满足上述条件 4,则 hi 既要取 hi 又要取 Thi,所以这个 T 对于 hi 不可行。

那么我们就可以对于每个 i 求出 (Li,Ri) 表示区间里有这个 i 会对 T 产生的限制:

  • himax(hi1,hi+1),Tmin(hi1,hi+1)+hi
  • himin(hi1,hi+1),Tmax(hi1,hi+1)+hi

那么一个区间合法当且仅当 maxLi<minRi。可以枚举左端点并二分右端点求解。

点击查看代码
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-]

简单题啊。

一个经典结论是 (nm)[nANDm=m](mod2)

proof

根据卢卡斯定理有 (nm)(n/pm/p)(nmodpmmodp)

然后我们观察到如果 n 最低位是 0m 最低位是 1,那么这个就是 0 了。

否则这个不会变为 0 就是 1

然后就能写出转移方程:fi=aiANDaj=ai{fj+1},使用枚举子集即可 O(3logV) 转移。

点击查看代码
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 是无解的。

proofn=3 时,设序列为 {a,b,c},变化后为 {b,a+c,b},显然二者总和不等,故无解。

n=5 时,设序列为 {a,b,c,d,e},变化后为 {b,a+c,b+d,c+e,d}b=bd=da+ca,c 只能 =ec+e=a,所以 b+d=c;又二者总和相减得 b+c+d=0,所以 c=0,无解。

然后考虑一个合法的长度为 n2 的序列,设其最后两项为 a,b,变化后的最后两项为 t,a

那么若在序列末尾添加 x,y,则 {a,b,x,y}={t,a+x,b+y,x},解得可取 x=b,y=ab

那么我们只用找到 n=2n=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 的限制该怎么做:倒着做,设 fi 表示赢后 i 个选手的最多奖金,有转移:

fi=max{fj+ijj}=max{1ij+fj1}

可以斜率优化。具体地,用单调队列维护 (j,fj1) 的上凸壳,斜线斜率是 1i 由大到小,故凸壳斜率递减。

然后加上 k 的限制就是 wqs 二分。二分斜率 k0,用这条直线去切答案下凸壳,切到的点是 (k,fn,k) 即可取答案。二分后的答案应尽量大,所以当可行时 midl

点击查看代码
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 度数 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 [提高+/省选-]

显然可以四维数论分块。

但是发现对于 bx,dx 相同的 x[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(nlogn)

点击查看代码
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 放对位置后,还剩下若干 ab 的位置。我们可以发现 0 在环上绕圈相当于其他位置进行一个轮换。那么求出这些点的 lca p。若 p 与这些点构成一条链,那么就要把链加一条边连成环;否则无解。然后如果环上除掉 p 以外的点 a,b 不构成轮换,无解;否则,步骤为 0 初始位置 0 目标位置 p,然后顺时针或逆时针绕圈,最后返回 0 目标位置。

但是,有可能有的路径可以省去!比如 p0 初始位置 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 的期望为 RAND_MAXk+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]

虚树+推式子。

1n(n1)i=1njiφ(aiaj)dist(i,j)

我们有 φ(xy)=φ(x)φ(y)(x,y)φ((x,y))

proof

φ(n)φ(m)=ni|n(i1i)mj|m(j1j)

稍微变形,

=nmi|nm(i1i)j|(n,m)(j1j)=φ(nm)gcd(n,m)φ(gcd(n,m))

所以原式可以变为:

i=1njiφ(ai)φ(aj)gcd(ai,aj)φ(gcd(ai,aj))dist(i,j)

枚举 gcd

d=1ndφ(d)i=1njiφ(ai)φ(aj)[gcd(ai,aj)=d]dist(i,j)

设:

f(d)=i=1njiφ(ai)φ(aj)[gcd(ai,aj)=d]dist(i,j)

等于不好算,换成倍数,设:

F(d)=i=1njiφ(ai)φ(aj)[d|ai,d|aj]dist(i,j)

反演:

f(d)=d|xF(x)μ(xd)

考虑计算 F(d)。把树上所有 d|ai 的点拎出来建虚树,消掉中括号。有 dist(i,j)=depi+depj2deplca(i,j),所以:

F(d)=iVvtjiφ(ai)φ(aj)(depi+depj2deplca(i,j))

变形,得:

F(d)=2iφ(ai)depijφ(aj)2ijφ(ai)φ(aj)deplca(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,m3000,考虑枚举第 k 大边权值 W

对于枚举的 W,令每条边权值 w=max(0,wW),然后跑最短路,用 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)(ijk) 必败,当且仅当 i==jkj 的质因数分解中有偶数个 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:设 fi,j 表示前 i 位,第一个串还有 j 位没有匹配的方案数;gi,j 表示同样状态的贡献和。f 的转移显然,g 的转移:gi,k=fi1,j|j|+gi1,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。上界是 cnt0×cnt1×max(u,v)Ewu,v
  • 否则,只有染色不同的两个棋子之间可能距离 1。上界是 (cnt0,1×cnt1,0+cnt0,0×cnt1,1)max(u,v)Ewu,v

可以证明只有原图是链的情况达不到上界。

proof

若图中存在环,首先可以将所有棋子移动到环上。环上的所有棋子都可以任意选择一个方向移动一步,就可以将所有棋子聚在一起。

若不存在环且不是链,则存在一个度数 3 的点。反证,若存在三个棋子使得其中任意两个永远不可能在同一个位置,则我们可以将他们移动到这个点以及它的两个邻点上,接着再次操作一个没有棋子的邻点,使得其中两个在同一个位置,矛盾。

最后是链的情况,发现两个棋子可行的距离与原距离同奇偶且不大于原距离。于是可以设 fi,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]

1rk+1i=krAi=SrSk1r(k1)

所以从右往左维护 (i,Si) 构成的上凸壳即可。

点击查看代码
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,终点所在点集为 Tf(S,T)S,T 的割集使 T 中点不能到 S。显然有 f(S,T)f(T,S)=iSdegi,outdegi,in

又因为竞赛图性质有 f(S,T)+f(T,S)=|S|×|T|。所以 f(S,T)=|S|×|T|+iSdegi,outdegi,in2。所以枚举 |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 使得 ρ(a,b),即把二元关系写成边,构成若干个团和至少一个孤立点(一个点的自环不算孤立点)。

容易发现没有孤立点的话方案数是贝尔数 Bnlink。有孤立点我们枚举非孤立点数量得 ans=i=0n1(ni)Bi=Bn+1Bn

点击查看代码
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!

首先可以钦定 ij 列有 1,其他列没有 1,最后算出方案后乘上组合数更新答案。根据题目,若一个矩形左上、右下两个位置是 1,则四个角都要是 1,所以我们可以发现合法的方案是左下到右上的两个递增阶梯 a,bb 中所有位置都在 a,中,再把在 a 中不在 b 中的位置设为 1

image

那么就可以 dp:设 fi,l,r 表示第 i 列,[l,r] 行为 1 的方案数,发现可以转移到 fi,l,rfi1,p,q 需满足 pl,max(1,l1)qr,是一个矩形,可以二维前缀和优化。

点击查看代码
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. 竞赛图的任意一个点数 3 的 SCC 中存在三元环 (u,v)(v,w)(w,u)

两个引理都是竞赛图的基本性质。

对这个竞赛图进行缩点,设链上的 SCC 点集依次为 s1,s2,...,sp,则若存在 i 使 i<p,|si|3,则选择一个属于该 SCC 的三元环 (u,v,w) 以及 sp 中的一个点 z(u,v,w,z) 构成一个子图。所以 i<p,|si|<3;又因为不存在大小为 2 的 SCC,所以 i<p,|si|=1

枚举 p,问题转化为求大小为 Np1、不存在给定子图、每个点入度 Kp1 的 SCC 的数量。设 n=Np1,m=Kp1

考虑任选 SCC 中一点 x,则其余 n1 个点可以被分为两个集合 S,T,其中 S 中点都有连向 x 的边,x 所有连出去的边都连向 T 中的点。由于 x 在 SCC 中,所以 |S|,|T|1

容易发现 S 一定构成一条链。否则 S 中会存在大小 3 的 SCC,其中取出一个三元环,这三个点与 x 会构成一个子图。

T 也一定构成一条链。否则 T 中会存在大小 3 的 SCC,其中取出三元环 (u,v,w)。设 S 链的第一个节点为 sT 的子集 T1 中的点连向了 ss 连向了 T 的子集 T2 中的点。若 |T2|=0,则 u,v,wT1(u,v,w,s) 构成子图;若 |T1|=0,则整个图构成不了 SCC;若存在 aT1,bT2 使得存在 (a,b),则 (a,s,x,b) 构成子图;否则不存在 T2T1 连的边,图构成不了 SCC。

把两条链上的节点按照拓扑序重标号。可以发现对于 siS,其连向 T 的节点一定是一个前缀。否则存在 tj,tj+1T 存在边 (tj,si),(si,tj+1)(si,tj,x,tj+1) 构成子图。设 pi 表示 jpi 存在边 (si,tj) 且不存在 (si,tj+1)

可以发现 pipi+1,p1<|T|

p1=|T|,则图构成不了 SCC。

pi>pi+1

  • pi<|T|,则 (tpi,tpi+1,si,si+1) 构成子图。
  • pi=|T|,则 (t|T|,s1,si,si+1) 构成子图。

所以就可以 dp。设 fi,j 表示左边第 i 个点的 pi=j 的方案数。枚举左边点数 p,则右边点数为 np1。转移时用 fi1,k 更新 fi,j(kj)

注意一个状态 fi,j 可行当且仅当:

  • i1jq
  • (qj)+(i1)m,即 si 的入度 m
  • (pi+1)+1+(j1)m,即 tj 的入度 m

最后设 sum=i=0qfp,i,即为左边 p 个点右边 q 个点的方案数。对答案的贡献要乘上 (n1p)p!q!,因为两边点可以随便排列,还要选择每个点到底在哪一边。注意这里是选择了中间点 x,所以 x 是唯一的,不用管。

现在求出 ansn=Np1,m=Kp1 的答案,对总答案的贡献要乘上 (np)p!,表示选择前面链上的若干个点。

点击查看代码
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),不妨 xy 的父亲,则使用并查集维护 x 到最浅的一个节点 tp 使得 xtp 的路径都是添加过的。然后用树剖将这些点的 siz 都加上 sizy

  • 若为查询操作 (x,y),不妨 xy 的父亲,则两部分大小分别为 siztpxsizy,sizy

点击查看代码
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。

fi,j,k 表示 cnti,填了 j 种数,k 个位置的方案数。pri 表示 [aji],则出现次数 i 的数只能填在 pri 个位置上且只有 pri 个数出现次数可以 i

发现随着 i 的减小,可填的区间逐步增加且完全包含更大的 i 可填的区间。所以 dp 方程:

fi,j+x,k+ix=xfi+1,j,k×(prijx)(prik)!(prikix)![(i)!]k

其中左边组合数表示从 pri 个可行数去掉已经选了的 j 个数中选择 x 个数。右边分式表示从 prik 个位置填上 ix 个数,且这些数分为 i 个集合,每个集合内无序的可重集方案数。

分析复杂度:看似是 O(n4),其实 j,xni

所以总共的循环次数大约为 i=1nn3i2=O(n3)

点击查看代码
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 种牌的数量为:i=1nci×2i×i!。设初始状态的数量为 st,对应每个牌堆的状态为 vli。则我们每次可以让 st 加上任意一个 vli;又因为每 2n×n! 个第 1 种牌又可以合成 1 个第 1 种牌,所以每次还可以让 st 减去 2n×n!1

所以最终可以到达的状态 ed 可以表示为 st+i=1nxiciy(2nn!1)。其中 xi,y 为任意自然数。由裴蜀定理,ed 可以到达的充要条件为 edst(modg),其中 g=gcd(gcdi=1nvli,(2nn!1))

cnt(x) 表示 x 张第 1 种牌经过若干次操作 1 的最小牌数。

这时候我们有两种方法:

  • 暴力枚举 ed=stmodg+kg(ed>0,kN),计算 cnt(ed) 最小值。复杂度 O(2nn!1g)
  • 同余最短路,建 g 个点,每个点 x 表示 ax(modg) 的最小的 cnt(a)。初始状态 dis2ii!modg=1,因为 dis0 不能取到 0。连边 i(i+2jj!)modp,边权为 1,表示添加一张第 j 种牌。最后答案取 disstmodg。复杂度 O(g)

总复杂度 O(2nn!1)。但是小于 2nn!1 且为 2nn!1 因数的最大值为 1214827,当 n=12 时取到。所以 g1214827 或者 g2nn!11214827。可以过。

点击查看代码
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,nR] 是上升的则统计答案;否则答案为 0

我们将 m 次操作分配给这 L+R 个数,方案数是 {mL+R}(L+RL)。然后这些操作中只有每个数的最后一次有方案限制,其它两个方向都可以,乘上 2mLR

点击查看代码
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,mk,则这个方案数只与 n+m,k 有关。

考虑把 ( 写成向量 (1,1)) 写成向量 (1,1),则括号串可与 (0,0)(n+m,nm) 的一条折线构成双射;括号串最多 k 组匹配等价于折线最低点纵坐标为 km(从 x 轴所有位置向右边发一束光,光能照到的 (1,1) 向量共 mk 个一定不能匹配,其它的都可以匹配,则得证)。

最低点纵坐标为 km 不好做,考虑求最低点纵坐标 km,即匹配数 k 的方案数。

考虑折线与 x=km 的直线的最后一个交点,并把之后的部分翻转。因为 x=nmx=km 的距离为 nk,所以翻转过后折线终点会变为 (n+m,2knm)。折线总数为 (n+mk)。这种折线可以与最低点纵坐标 km 的折线构成双射。

所以答案为 (n+mk)(n+mk1)

another problem

n,对于所有 k[0,n],求 k 个左括号 nk 个右括号构成的括号序列,最长合法括号子序列的长度之和。

考虑递推,设 fi 表示 k=i 时的答案。则 fii 个左括号构成最长合法括号子序列长度 <i,=i 的答案加起来。由于上文提到的结论,那么 <i 的答案就是 fi1!然后 =i 的部分就变成了上面的题,方案数是 (ni)(ni1),贡献是 i((ni)(ni1))

这个递推只适用于 2in+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]

将数组从小到大排序。设 fi,j 表示前 i 个数划分为 j 个圆排列的答案。容易得到递推式:

fi,j=fi1,j1ai+(i1)fi1,j

考虑生成函数 Gi=j=1nfi,jxj,则有:

Gi=aixGi1+(i1)Gi1=Gi1(aix+i1)=j=1i(ajx+j1)

bi=[xi]Gn,则答案为 gcdi=1n[xi]Gn

有结论:设 c(F) 表示 gcdi[xi]F,则 c(FG)=c(F)c(G)

proof

考虑 c(F)=c(G)=1 时,若 c(FG)=p1,则 FG0(modp),又 F,G0(modp),矛盾。则 c(FG)=1

所以 gcdi=1n[xi]Gn=i=1ngcd(ai,i1)

点击查看代码
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-]

可以发现对于最优方案,每个选择的区间两边端点颜色相同。所以 设 fi 表示 [1,i] 分成若干区间的最优答案。则 fi 只会从 aj+1=aifj 转移过来。设 cnti 表示位置 i 是数组中出现的第几个 ai

fi=maxaj=ai{fj1+ai(cnticntj+1)2}

拆开:

fi=aicnti2+maxaj=ai{fj1+ajcntj2+2cntj×(cnti1)}

可以把每个颜色分开来斜率优化。每个点的坐标为 (2cntj,fj1+ajcntj2),考虑是上凸还是下凸/取首位还是末位:

  • 由于斜率 cnti1 变小,所以维护下凸;
  • 由于要取下凸最大值,所以答案取栈顶。

所以维护若干单调栈:

  • 弹出栈顶若干元素并插入 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,每个数至多包含一个 47 的质因数,考虑把质数分成 4347 两部分处理。对于每个数预处理出质因数拆分 (st,x),其中 st 为一个 14 位的二进制数,表示是否包含 [2,43] 内的质数,x 表示这个数除掉 43 的质数后剩余的部分。然后把这些数按照 x=1,47,53,... 分成若干类,每一类分别计算后合并答案。

简单的 dp 是设 fi,j 表示第 i 类中的数乘积包含状态 j 的质因数的方案数。这个很容易 dp,但是并不容易合并。由之前模拟赛爆炸的经验得这里可以先容斥,合并后再统计回来。设 fi,j 表示第 i 类中的数乘积不包含状态 j 的质因数的方案数。我们可以先 dp 出 gi,j 表示第 i 类中的数乘积不包含状态 j 的质因数,且包含除掉状态 j 以外的所有 [2,43] 内的质因数的方案数,然后利用 FWT 即可算出 fi,j

由于 fi,j 的定义,我们要合并两个状态 p,q 直接 fp,j×fq,j 即可。但是还有一个细节:fi,j 并没有要求要包含第 i 类表示的质因数 。所以对于一个询问的质因数集合 S,若第 i 类表示的质因数在 S 中,每个 fi 需要减一,表示去掉选择空集的方案。

容斥系数为 (1)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 长度为 kn 阶排列个数等价于第一行 k 个格子的杨表数的平方。所以我们枚举杨图,并使用勾长公式计算出杨图对应的杨表数即可。复杂度 n3 乘上 n 的拆分数,可以过。

勾长公式:cnt=n!di,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-]

ajai+pi|ij| 可得 piaj+|ij|ai。把 >j,<ji 分开来得:

pi=max{maxj<iaj+ijai,maxj>iaj+jiai}

首先只处理左半部分,设 fj(x)=aj+xj,则每次转移要选使得 fj(i) 最大的 j 转移到 i

把这些函数图像画出来得:

image

发现最优的函数一定会是反超之前的所有函数,后来又被新出现的函数反超。

所以维护一个单调队列,其中相邻两个函数的交点横坐标递增。插入一个函数的时候求出它与队首函数的交点,然后把交点横坐标大于这个交点的队内函数出队。

转移的时候查看队首的若干个交点,把交点横坐标小于 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,设 fi,j,S 表示前 i 个叶子,到根路径状态为 S,选了 j 个战争叶子的最优解,容易发现复杂度至少是 O(8n) 的,过不去。

还有一个做法是:dfs 遍历每个点的同时记录根到点路径的选择方案,遍历到叶子的时候根据选择方案算出叶子的答案,回溯的时候使用 dp:fx,i+j=max{fx<<1,i+fx<<1|1,j} 更新各个节点的答案。这个的复杂度是 i=1n2i1(2ni1)22i1+2n12n1n=O(n4n) 的,可以通过。

这两个做法看起来很相似,但是为什么复杂度不同?或者说第一个做法多计算了哪些部分?

实际上,对于根的两棵子树 2,32 最右侧的链的状态对于 3 是完全没有影响的。统计一下发现多记录的状态和为 O(2n+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);
}
posted @   KiharaTouma  阅读(54)  评论(1编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起