做题笔记 - Mar. 2026
题目整理
[ARC216] D - GCD of Product of Arithmetic Progression
定义 \(\displaystyle f(x)=\prod_{i=0}^{N-1}(Bx+Di+C)\),即 \(f(x)=a_x\),记 \(v_p(n)\) 表示 \(n\) 中存在 \(v_p(n)\) 个质因子 \(p\),答案 \(\displaystyle G=\gcd_{i=0}^N f(i)\)。
首先如果 \(g=\gcd(B,C,D)\neq 1\),则可以让答案变大 \(g^N\) 倍,同时令 \(B,C,D\) 除以 \(g\),因此以下讨论基于 \(\gcd(B,C,D)=1\)。
考虑对所有质数 \(p\) 计算 \(v_p(G)\),即分开处理每个质数 \(p\) 造成的贡献,并讨论 \(B,D\) 是否是 \(p\) 的倍数。
-
若 \(p\mid B\) 且 \(p \mid D\),则有 \(p\nmid C\),因此 \(\forall x,i,~p\nmid Bx+Di+C\),\(v_p(G)=0\)。
-
若 \(p\nmid B\) 且 \(p\mid D\),则若 \(\exists x,~p\mid f(x)\),必定有 \(p\mid Bx+C\),而由 \(p\nmid B\) 可得 \(p\nmid B(x\pm 1)+C\),因此 \(p\nmid f(x\pm 1)\),\(v_p(G)=0\)。
-
若 \(p\nmid B\) 且 \(p\nmid D\),则 \(v_p(G)=v_p(N!)\),接下来考虑证明这一点。
-
因为 \(\forall k>0,\displaystyle \sum_{i=0}^{N-1} [p^k\mid Bx+Di+C]\geq \left\lfloor\frac N{p^k}\right\rfloor = \sum_{i=1}^N [p^k\mid i]\),
所以 \(\displaystyle v_p(G)= \min_{i=0}^N v_p(f(i))=\min_{i=0}^N\sum_{k=1}^\infty\sum_{i=0}^{N-1} [p^k\mid Bx+Di+C]\geq \sum_{k=1}^\infty\left\lfloor\frac N{p^k}\right\rfloor = \sum_{k=1}^\infty\sum_{i=1}^N [p^k\mid i]=v_p(N!)\)。
-
考虑引入以下算子:
- 恒等算子 \(I\):\(If(x)=f(x)\)。
- 移位算子 \(E\):\(Ef(x)=f(x+1)\)。
- 差分算子 \(\Delta\):\(\Delta f(x)=f(x+1)-f(x)=(E-I)f(x)\)。
则 \(f\) 的 \(n\) 阶差分 \(\displaystyle \Delta^Nf(x)=(E-I)^Nf(x)=\sum_{i=0}^N \binom{N}{i}E^i(-I)^{N-i}f(x)=\sum_{i=0}^N (-1)^{N-i}\binom{N}{i}f(x+i)\) 。而 \(f(x)=B^Nx^N+r\),其中余项 \(r\) 是一个关于 \(x\) 的 \(n-1\) 次多项式,因此 \(\Delta f(x)=B^NNx^{N-1}+r'\),归纳可得 \(\Delta^N f(x)=B^NN!\)。已知 \(\Delta^N f(0)\) 是一个关于 \(f(0),f(1),\ldots,f(N)\) 的整系数线性组合,因此 \(v_p(G)\leq v_p(\Delta^Nf(0))=v_p(B^NN!)=v_p(N!)\)。
故 \(v_p(G)=v_p(N!)\)。
-
-
若 \(p\mid B\) 且 \(p\nmid D\),则考虑按 \(M=v_p(B)\) 进行阈值分治,记 \(\displaystyle h_p(x,k)=\sum_{i=0}^{N-1}[p^k\mid Bx+Di+C]\):
- 对于 \(k\leq M\),\(\displaystyle h_p(x,k)=\sum_{i=0}^{N-1}[p^k\mid Bx+Di+C]=\sum_{i=0}^{N-1}[p^k\mid Di+C]\)。因此这部分对 \(v_p(G)\) 共造成 \(\displaystyle \sum_{i=1}^M h_p(0,i)\) 的贡献。
- 对于 \(k> M\),考虑构造一组 \(\displaystyle B'=\frac B{p^M},C',D'\),使得 \(\forall p^k\mid Bx+Di+C,~Bx+Di+C\equiv p^k(B'x+D'i+C)\)。可以证明这样的 \(B',D',C'\) 一定存在,同时一定有 \(p\nmid B',p\nmid D'\),因此这部分的贡献为 \(v_p(h_p(0,M)!)\)。
合起来就是 \(\displaystyle v_p(G)=\sum_{i=1}^M h_p(0,i)+v_p(h_p(0,M)!)\)。
由于绝大多数情况下 \(p\nmid B,p\nmid D\),因此考虑答案在 \(N!\) 上修改。由于 \(v_p(N!)\) 和 \(h_p(n,k)\) 均可在单次 \(O(\log_p N)\) 的时间复杂度计算,故总时间复杂度为 \(O(T\log^2 N)\)。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll=long long;
const int N=1e6+9;
const int mod=998244353;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ull*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ull*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int fac[N],vis[N];
vector<int> pri[N];
inline void Init(int lim){
fac[0]=fac[1]=1,vis[1]=1;
for(int i=1;i<=lim;i++){
fac[i]=Mul(fac[i-1],i);
if(vis[i]) continue ;
for(int j=i;j<=lim;j+=i){
vis[j]=1;
pri[j].push_back(i);
}
}
}
inline void ExGCD(int a,int b,int &x,int &y){
if(!b) return x=1,y=0,void();
int _x,_y;
ExGCD(b,a%b,_x,_y);
x=_y,y=_x-_y*(a/b);
}
int n,b,c,d;
inline void Solve(){
cin>>n>>b>>c>>d;
int ans=fac[n],g=__gcd(__gcd(b,d),c);
MulAs(ans,QPow(g,n));
b/=g,c/=g,d/=g;
vector<int> ps;
ps.insert(ps.end(),pri[b].begin(),pri[b].end());
ps.insert(ps.end(),pri[d].begin(),pri[d].end());
sort(ps.begin(),ps.end());
ps.erase(unique(ps.begin(),ps.end()),ps.end());
for(int p:ps){
if(d%p==0){
int cnt=0,t=n;
while(t) cnt+=(t/=p);
MulAs(ans,QPow(Inv(p),cnt));
}else{
int cnt=0,t=n,i,pk,x,y;
while(t) cnt+=(t/=p);
for(i=1,pk=p;b%pk==0;i++,pk*=p){
ExGCD(d,pk,x,y);
x=1ull*(x%pk+pk)%pk*(pk-c%pk)%pk;
cnt-=(n+pk-x-1)/pk;
}
i--,pk/=p,t=(n+pk-x-1)/pk;
while(t) cnt-=(t/=p);
MulAs(ans,QPow(cnt>=0?Inv(p):p,cnt>=0?cnt:-cnt));
}
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
Init(1e6);
int T;
cin>>T;
while(T--) Solve();
return 0;
}
[集训队作业 2018] count
首先区间最大值位置全部相同等价于笛卡尔树形态相同,那么限制变为左子树方向边权为 \(1\),右子树方向权值为 \(0\),深度不超过 \(m\)。
考虑把右儿子变成父亲的兄弟节点,那么整颗树深度就不超过 \(m\),且与原树一一对应。
而深度不超过 \(m\) 的无标号区分儿子的树则可以对进栈出栈序列计数,相当于从 \((0,0)\) 走到 \((n,n)\),不能碰到 \(y=x+1\) 和 \(y=x-m-1\)。反射容斥即可,时间复杂度 \(O\left(\dfrac nm\right)\)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+9;
const int mod=998244353;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ll*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ll*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int fac[N<<1],ifac[N<<1],n,m;
inline void Init(int lim){
fac[0]=1;
for(int i=1;i<=lim;i++) fac[i]=Mul(fac[i-1],i);
ifac[lim]=Inv(fac[lim]);
for(int i=lim-1;~i;i--) ifac[i]=Mul(ifac[i+1],i+1);
}
inline int C(int n,int m){
if(m<0||m>n) return 0;
else return Mul(fac[n],Mul(ifac[m],ifac[n-m]));
}
signed main(){
cin>>n>>m;
if(m>n) return cout<<0<<endl,0;
Init(n<<1);
int ans=C(n<<1,n);
for(int x=n,y=n,p=1,q=-m-1,f=1;C(x+y,x);f^=1){
swap(x,y),x-=p,y+=p;
swap(p,q),p=2*q-p;
f?SubAs(ans,C(x+y,x)):AddAs(ans,C(x+y,x));
}
for(int x=n,y=n,p=-m-1,q=1,f=1;C(x+y,x);f^=1){
swap(x,y),x-=p,y+=p;
swap(p,q),p=2*q-p;
f?SubAs(ans,C(x+y,x)):AddAs(ans,C(x+y,x));
}
cout<<ans<<endl;
return 0;
}
[集训队作业 2018] 喂鸽子
记喂一次饱了的鸽子为一次无效操作,反之为有效操作。考虑到某只鸽子饱了的时候再去分配其对应的有效操作次数,同时在分配过程中不区分鸽子。
设 \(E_{i,j}\) 表示已经喂饱 \(i\) 只鸽子,还剩 \(j\) 次有效操作没被分配的期望时间,\(P_{i,j}\) 表示该事件发生的概率,则每次发生有效操作的期望时间即为 \(\dfrac {n}{n-i}\),有转移:
- 该次操作没喂饱被喂的鸽子:对于某个特定的鸽子,其被选中的概率为\(\dfrac 1{n-i}\),因此有 \(E_{i,j+1}\leftarrow \dfrac 1{n-i}\left(E_{i,j}+\dfrac n{n-i}P_{i,j}\right),~P_{i,j+1}\leftarrow \dfrac {P_{i,j}}{n-i}\)。
- 该次操作喂饱了某只鸽子:\(E_{i+1,j-k+1}\leftarrow \dfrac 1{n-i}\left(E_{i,j}+\dfrac n{n-i}P_{i,j}\right)\dbinom{j}{k-1},~P_{i+1,j-k+1}\leftarrow \dfrac {P_{i,j}}{n-i}\dbinom{j}{k-1}\)。
时间复杂度 \(O(n^2k)\)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e3+9;
const int K=5e1+9;
const int mod=998244353;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ull*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ull*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int fac[N*K],ifac[N*K];
inline void Init(int lim){
fac[0]=1;
for(int i=1;i<=lim;i++) fac[i]=Mul(fac[i-1],i);
ifac[lim]=Inv(fac[lim]);
for(int i=lim-1;~i;i--) ifac[i]=Mul(ifac[i+1],i+1);
}
inline int C(int n,int m){
if(m<0||m>n) return 0;
else return Mul(fac[n],Mul(ifac[m],ifac[n-m]));
}
int f[N][N*K],p[N][N*K],inv[N],n,k;
signed main(){
cin>>n>>k;
Init(n*k);
f[0][0]=0,p[0][0]=1;
for(int i=1;i<=n;i++) inv[i]=Inv(i);
for(int i=0;i<=n;i++){
for(int j=0;j<=(n-i)*k;j++){
int t=Mul(n,inv[n-i]);
AddAs(f[i][j+1],Mul(Add(f[i][j],Mul(p[i][j],t)),inv[n-i]));
AddAs(p[i][j+1],Mul(p[i][j],inv[n-i]));
if(j+1>=k){
AddAs(f[i+1][j+1-k],Mul(Mul(Add(f[i][j],Mul(p[i][j],t)),inv[n-i]),C(j,k-1)));
AddAs(p[i+1][j+1-k],Mul(Mul(p[i][j],inv[n-i]),C(j,k-1)));
}
}
}
int ans=f[n][0];
for(int i=1;i<=n;i++) MulAs(ans,i);
cout<<ans<<endl;
return 0;
}
[UR #23] 地铁规划
单栈模拟队列模板。
粗浅地定义即将出栈的元素为 \(0\) 元素,刚加入的元素为 \(1\) 元素,\(0\) 元素依进栈时间向栈顶升序排序,\(1\) 元素依进栈时间向栈顶降序排序。任意 \(0\) 元素的进栈时间一定比 \(1\) 元素少。
插入某个元素时将其标记为 \(1\) 元素并直接放入栈内。
删除最早出现的元素时不断弹出元素直到 \(0\) 元素不少于 \(1\) 元素,如果 \(1\) 元素始终比 \(0\) 元素多则将所有元素标记为 \(0\) 元素并重构整个栈,否则先依序加入 \(1\) 元素,再依序加入 \(0\) 元素。最后在栈顶的一定是 \(0\) 元素,根据定义,该元素即为现在栈内进栈时间最小的元素。
在整个过程中,\(0\) 元素到栈顶的距离每次至少减半,且每次操作次数和操作结束时本次涉及到的 \(0\) 元素个数线性相关,因此总操作次数是 \(O(n\log n)\) 级别的。
#include<bits/stdc++.h>
#include"subway.h"
using namespace std;
int n,m,lim;
vector<array<int,2>> stk;
inline void Push(int x){
merge(x);
stk.push_back({x,1});
}
inline void Pop(){
vector<int> s[2];
while(stk.size()){
s[stk.back()[1]].push_back(stk.back()[0]);
stk.pop_back();
undo();
if(s[0].size()>=s[1].size()){
reverse(s[0].begin(),s[0].end());
reverse(s[1].begin(),s[1].end());
s[0].erase(s[0].end()-1);
for(int x:s[1]) stk.push_back({x,1}),merge(x);
for(int x:s[0]) stk.push_back({x,0}),merge(x);
return ;
}
}
reverse(s[0].begin(),s[0].end());
if(s[0].size()) s[0].erase(s[0].end()-1);
else s[1].erase(s[1].end()-1);
for(int x:s[1]) stk.push_back({x,0}),merge(x);
for(int x:s[0]) stk.push_back({x,0}),merge(x);
}
int r;
void init(int _n,int _m,int _lim){n=_n,m=_m,lim=_lim;}
int solve(int l){
while(r<m&&check(r+1)) Push(++r);
Pop();
return r;
}
[UNR #6] 小火车
首先 \(p<2^n\) 保证了一定有解,同时提示做法类似找到两个和相同的集合,并将一个集合附上 \(1\) 的权,另一个附上 \(-1\) 的权。
\(n=40\) 容易想到折半搜索,但是直接做肯定不好做。考虑先折半预处理出高位集合和低位集合的所有取值,然后二分答案,将区间 \([L,R)\) 分成 \([L,M)\) 和 \([M,R)\),并计算两边各选一个数,和在 \([L,M)\) 中的二元组个数 \(C([L,M))\),这是可以双指针的。在 \(C([L,R))> R-L\) 的情况下,\(C([L,M))>M-L\) 和 \(C([M,R))>R-M\) 必然满足至少一个。而 \(C([l,r))>r-l\) 则意味着根据鸽巢原理,必然有两个集合和相同,且和在 \([l,r)\) 内。因此问题规模缩小一半。在已知答案的情况下构造解是容易的。时间复杂度 \(O(n2^{\frac n2})\)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=4e1+9;
const int S=(1<<20)+9;
mt19937_64 rng(4649);
int p[S],q[S],n;
ll a[N],x[S],y[S],u[S],v[S],mod;
signed main(){
cin>>n>>mod;
for(int i=0;i<n;i++) cin>>a[i];
int X=1<<(n>>1),Y=1<<(n+1>>1);
for(int i=1;i<X;i++){
int j=__lg(i);
x[i]=(x[i^(1<<j)]+a[j])%mod;
}
for(int i=1;i<Y;i++){
int j=__lg(i);
y[i]=(y[i^(1<<j)]+a[j+(n>>1)])%mod;
}
iota(p,p+X,0),iota(q,q+Y,0);
sort(p,p+X,[&](int i,int j){return x[i]<x[j];});
sort(q,q+Y,[&](int i,int j){return y[i]<y[j];});
for(int i=0;i<X;i++) u[i]=x[p[i]];
for(int i=0;i<Y;i++) v[i]=y[q[i]];
ll L=0,R=mod-1;
while(L<R){
ll M=L+R>>1;
ll cnt=0;
for(ll d:{0ll,mod}){
for(int i=X-1,j=0,k=0;~i;i--){
while(j<Y&&u[i]+v[j]<=M+d) j++;
while(k<Y&&u[i]+v[k]<L+d) k++;
cnt+=j-k;
}
}
if(cnt>M-L+1) R=M;
else L=M+1;
}
vector<ll> sta;
for(ll d:{0ll,mod}){
for(int i=X-1,j=0,k=0;~i&&sta.size()<2;i--){
while(j<Y&&x[p[i]]+y[q[j]]<=L+d) j++;
while(k<Y&&x[p[i]]+y[q[k]]<L+d) k++;
for(int o=k;o<j&&sta.size()<2;o++) sta.push_back(ll(q[o])<<(n>>1)|p[i]);
}
}
for(int i=0;i<n;i++) cout<<(sta[0]>>i&1)-(sta[1]>>i&1)<<' ';cout<<endl;
return 0;
}
[Ptz W 2022 Day2] K. Fake Plastic Trees 2
首先很容易想到设计 \(f_{u,i,j}\) 表示当前考虑到 \(u\) 子树,断了 \(i\) 条边,含根连通块权值和为 \(j\)。但是 \(j\) 这一维十分巨大,因此考虑削减状态数。由于要求仅为剖出去的连通块权值在 \([L,R]\) 中,因此对于 \(j_1<j_2<j_3\leq j_1+(R-L)\),不存在任意一种转移方式使得 \(j_2\) 能成功转移而 \(j_1,j_3\) 在经过相同的转移之后非法。因此状态数就减少了 \(\dfrac {R-L}2\) 倍,而原来的状态必然在 \([S_u-iL,S_u-iR]\) 内,其中 \(S_u\) 表示 \(u\) 子树内权值和,因此对于相同的 \(u,i\),不同的 \(j\) 至多只有 \(2i\) 个。此时状态数降到 \(O(NK^2)\),时间复杂度 \(O(NK^3)\)。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll=long long;
const int N=1e3+9;
const int K=5e1+9;
ll a[N],L,R;
vector<int> e[N];
vector<ll> f[N][K],g[K];
int fa[N],siz[N],n,k;
inline void DFS(int x){
siz[x]=1;
if(a[x]<=R) f[x][0].push_back(a[x]);
for(int y:e[x]){
if(y==fa[x]) continue ;
fa[y]=x;
DFS(y);
for(int i=0;i<=min(siz[x],k);i++){
for(int j=0;j<=min(siz[y],k);j++){
if(f[y][j].size()&&f[y][j].back()>=L&&i+j<k){
for(ll p:f[x][i]) g[i+j+1].push_back(p);
}
if(i+j<=k){
for(ll p:f[x][i]){
for(ll q:f[y][j]){
if(p+q<=R) g[i+j].push_back(p+q);
}
}
}
}
}
siz[x]+=siz[y];
for(int i=0;i<=min(siz[x],k);i++){
f[x][i].clear();
sort(g[i].begin(),g[i].end());
for(ll p:g[i]){
if(f[x][i].size()>1&&p-f[x][i].end()[-2]<=R-L) f[x][i].pop_back();
f[x][i].push_back(p);
}
g[i].clear();
}
}
}
inline void Solve(){
cin>>n>>k>>L>>R;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
DFS(1);
for(int i=0;i<=k;i++) cout<<(f[1][i].size()&&f[1][i].back()>=L);cout<<endl;
for(int i=1;i<=n;i++){
e[i].clear();
for(int j=0;j<=min(siz[i],k);j++) f[i][j].clear();
}
}
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
int T;
cin>>T;
while(T--) Solve();
return 0;
}
[APIO 2022] Game
要求等价于存在一个包含链上节点的环,考虑先把链上的点拆点,这样就变成包含链上的边的环。
对于链上任意节点 \(i\),记可以到达 \([0,i)\) 的点构成的集合为 \(S_1\),可以被 \([i,k)\) 到达的节点构成的集合为 \(S_2\)。\([0,i)\) 天然地在 \(S_1\) 中,\([i,k)\) 天然地在 \(S_2\) 中。则答案环包含边 \((i-1,i)\) 的充要条件为 \(S_1\cap S_2\neq \varnothing\)。为了方便描述,令 \(c_x=\left\{\begin{matrix}1& x\in S_1\\2&x\in S_2\\ 0 & x\notin S_1\wedge x\notin S_2\end{matrix}\right.\),则加入边 \((u,v)\) 时:
- 若 \(c_u=2,c_v=1\),则出现包含 \((i-1,i)\) 的环,退出。
- 若 \(c_u=2,c_v=0\),则将 \(v\) 加入 \(S_2\),并检查所有 \(v\) 的出边。
- 若 \(c_u=0,c_v=1\),则将 \(u\) 加入 \(S_1\),并检查所有 \(u\) 的入边。
- 否则无事发生。
由于在出现包含链上边的环之前 \(S_1\cap S_2=\varnothing\),因此 \(c_x\neq 0\) 的所有点 \(x\) 被分在 \((i-1,i)\) 的两侧,即 \(c_u=c_v\neq 0\) 的边 \((u,v)\) 只会对一侧有用。则此时可以对 \([0,i)\) 和 \([i,k)\) 做类似的操作,即分治判断所有链边。时间复杂度 \(O(n\log n)\)。压位表示 \(c_x\) 空间复杂度即可做到 \(O(n)\)。
#include<bits/stdc++.h>
#include"game.h"
using namespace std;
const int N=3e5+9;
int id[N],n,k;
vector<int> e[N],r[N];
inline void Build(int x,int L,int R){
if(L==R) return id[L]=x,void();
int mid=L+R>>1;
Build(x<<1,L,mid),Build(x<<1|1,mid+1,R);
}
inline int NBel(int x,int y){return y<x||(y>>__lg(y)-__lg(x))!=x;}
inline int Get(int x,int y){return x<y?(y>>__lg(y)-__lg(x)-1&1)+1:0;}
inline int Modify(int x,int L,int R,int u,int v){
if(Get(x,id[u])==2&&Get(x,id[v])==1) return 1;
if(Get(x,id[u])==2&&Get(x,id[v])==0){
id[v]=id[v]<<1|1;
for(int p:e[v]){
if(NBel(x,id[p])) continue ;
if(Modify(x,L,R,v,p)) return 1;
}
}
if(Get(x,id[u])==0&&Get(x,id[v])==1){
id[u]=id[u]<<1;
for(int p:r[u]){
if(NBel(x,id[p])) continue ;
if(Modify(x,L,R,p,u)) return 1;
}
}
int mid=L+R>>1;
if(L==R) return 0;
if(Get(x,id[u])==1&&Get(x,id[v])==1) return Modify(x<<1,L,mid,u,v);
else if(Get(x,id[u])==2&&Get(x,id[v])==2) return Modify(x<<1|1,mid+1,R,u,v);
else return 0;
}
void init(int _n,int _k){
n=_n,k=_k;
Build(1,0,k);
for(int i=k+1;i<=n;i++) id[i]=1;
}
int add_teleporter(int u,int v){
u++,v+=(v>=k);
e[u].push_back(v);
r[v].push_back(u);
return Modify(1,0,k,u,v);
}
[EC Final 2023] A. DFS Order 4
为了把 DFS 序和树形态对应起来,考虑加一条限制:对于任意节点,其编号一定要比它前面的兄弟的最后一个儿子要小。不然可以把该节点的父亲变成它前面的兄弟而 DFS 序不变。那么把所有小于号的限制用有向边刻画出来,则原图变成一个 DAG。DAG 拓扑序是难做的,因此考虑对额外的限制容斥,将额外的边反向,则删去没用的边之后原图仍然是一颗外向树。
外向树的拓扑序树为 \(\dfrac {n!}{\prod_isiz_i}\),则在计数时向右加儿子会使根之前所有的儿子在新树上的 \(siz\) 变大,因此考虑向左在最开始加儿子。则子树大小会产生影响的就仅有新加的子树,以及如果存在额外限制则新加的子树的根节点的所有儿子也会产生变动。因此不妨在 DP 时就提前先加入额外限制的贡献,设 \(f_{i,j}\) 表示整体子树大小为 \(i\) 时,若后面因额外限制在新树上多加入一个大小为 \(j\) 的子树,所有可能的树的 \(\dfrac 1{\prod_isiz_i}\) 之和。
考虑枚举最左侧子树的大小,有转移:\(\displaystyle f_{i,j}=\sum_{k=1}^{i-1} \left(\dfrac {k}{i+j-1}f_{k,0}-[k+1<i]f_{k,i+j-k-1}\right)\cdot \dfrac {i+j-k}{i+j}f_{i-k,j}\)。
时间复杂度 \(O(n^3)\)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=8e2+9;
int mod;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ull*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ull*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int f[N][N],inv[N],n;
signed main(){
cin>>n>>mod;
for(int i=0;i<n;i++) f[1][i]=1,inv[i+1]=Inv(i+1);
for(int i=2;i<=n;i++){
for(int j=0;i+j<=n;j++){
for(int k=1;k<i;k++) AddAs(f[i][j],Mul(f[i-k][j],Sub(f[k][0],k<i-1?f[k][i-k+j-1]:0)));
MulAs(f[i][j],inv[i+j-1]);
}
}
int ans=Mul(inv[n],f[n][0]);
for(int i=1;i<=n;i++) MulAs(ans,i);
cout<<ans<<endl;
return 0;
}
[CCPC Final 2023] L. Exchanging Kubic
首先可以花 \(n\) 次问出每个数的正负性,且可以知道正数的具体取值。考虑把相邻的正数合并,相邻的负数合并,则对于相邻的 +-+,如果查出来的值比两侧的 + 都大,那么就可以得出 - 的和是什么。因为 - 连成了一段,因此事实上并不关心其取值,换言之可以任意负权只要和为定值。同时由于把 +-+ 连起来比单独 + 要优,所以可以把 +-+ 缩成一个 +,权值依然为区间和。
如果 +-+ 查出来是 + 的较大值,则可以说明 - 的绝对值一定比较小的 + 大。考虑找到最小的 +,向两边分别查 +-+,如果都没能缩段,那么说明这个 + 绝对值比两边的 - 都小,因此不会有区间的最大子段和是一边在这个 + 里,一边在这个 + 外的,因此考虑把这个 + 和两边的 - 连起来,缩成一个 -。而 - 怎么赋权都可以,只要保证 + 不会被外面连到且和固定即可,因此随便挑个位置(比如右边 - 的左端点)设成 + 的相反数再合并即可。
前面花了 \(n\) 次,后面每两次至少消掉两个段,因此总操作数不超过 \(2n\)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=2e3+9;
const ll inf=2e12;
ll a[N];
int b[N],n;
inline ll Q(int l,int r){
ll t;
cout<<"? "<<l<<' '<<r<<endl;
cin>>t;
return t;
}
template<class It> inline void Report(It l,It r){
cout<<"! ";for(It i=l;i!=r;i++) cout<<*i<<' ';cout<<endl;
}
inline void Solve(){
cin>>n;
for(int i=1;i<=n;i++){
a[i]=Q(i,i);
b[i]=bool(a[i]);
}
vector<array<ll,3>> v;
for(int i=1,j=1;i<=n;i++){
if(i+1>n||b[i+1]!=b[i]){
v.push_back({0,j,i});
if(b[i]) for(int k=j;k<=i;k++) v.back()[0]+=a[k];
else v.back()[0]=inf;
j=i+1;
}
}
if(v.size()&&!b[1]){
for(int i=v.front()[1];i<=v.front()[2];i++) a[i]=-inf;
v.erase(v.begin());
}
if(v.size()&&!b[n]){
for(int i=v.back()[1];i<=v.back()[2];i++) a[i]=-inf;
v.pop_back();
}
while(v.size()>1){
bool flag=0;
int x=min_element(v.begin(),v.end())-v.begin();
if(!flag&&x!=0){
ll t=Q(v[x-2][1],v[x][2]);
if(t>0&&t!=v[x-2][0]&&t!=v[x][0]){
a[v[x-1][1]]=t-v[x-2][0]-v[x][0];
for(int i=v[x-1][1];i<=v[x-1][2];i++) b[i]=1;
flag=1;
array<ll,3> tmp({t,v[x-2][1],v[x][2]});
v.erase(v.begin()+x-2,v.begin()+x+1);
v.insert(v.begin()+x-2,tmp);
}
}
if(!flag&&x!=v.size()-1){
ll t=Q(v[x][1],v[x+2][2]);
if(t>0&&t!=v[x][0]&&t!=v[x+2][0]){
a[v[x+1][1]]=t-v[x][0]-v[x+2][0];
for(int i=v[x+1][1];i<=v[x+1][2];i++) b[i]=1;
flag=1;
array<ll,3> tmp({t,v[x][1],v[x+2][2]});
v.erase(v.begin()+x,v.begin()+x+3);
v.insert(v.begin()+x,tmp);
}
}
if(flag) continue ;
for(int i=v[x][1];i<=v[x][2];i++) b[i]=0;
if(x==0){
a[v[x+1][1]]=-v[x][0];
v.erase(v.begin()+x,v.begin()+x+2);
}else if(x==v.size()-1){
a[v[x-1][1]]=-v[x][0];
v.erase(v.begin()+x-1,v.begin()+x+1);
}else{
a[v[x+1][1]]=-v[x][0];
array<ll,3> tmp({inf,v[x-1][1],v[x+1][2]});
v.erase(v.begin()+x-1,v.begin()+x+2);
v.insert(v.begin()+x-1,tmp);
}
}
Report(a+1,a+n+1);
}
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
int T;
cin>>T;
while(T--) Solve();
return 0;
}
[NHSPC 2025] 彩色游行
考虑对 \(r\) 扫描线,则需要对每个 \(l\) 维护 \([l,r]\) 内不同颜色个数。
由于每个位置的颜色是一个区间,因此可以由珂朵莉树动态维护颜色数,时间复杂度 \(O(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll=long long;
const int N=1e5+9;
const ll inf=1e18;
int cl[N],cr[N],n,k;
struct Data{
vector<ll> a;
inline void Init(){a.resize(k+1,-inf);}
inline ll& operator [](int pos){return a[pos];}
inline void operator +=(ll t){for(ll &x:a) x+=t;}
inline void operator <<=(int t){
for(int i=k;i>=t;i--) a[i]=a[i-t];
for(int i=t-1;~i;i--) a[i]=-inf;
}
friend inline Data operator |(Data x,Data y){
for(int i=0;i<k;i++) x[i]=max(x[i],y[i]);
return x;
}
};
struct Node{
Data dat;
ll tag;
}tr[N<<2];
inline void PushUp(int x){tr[x].dat=tr[x<<1].dat|tr[x<<1|1].dat;}
inline void Push(int x,ll k){tr[x].tag+=k,tr[x].dat+=k;}
inline void PushDown(int x){
if(tr[x].tag){
Push(x<<1,tr[x].tag);
Push(x<<1|1,tr[x].tag);
tr[x].tag=0;
}
}
inline void Build(int x,int L,int R){
if(L==R) return tr[x].dat.Init();
int mid=L+R>>1;
Build(x<<1,L,mid),Build(x<<1|1,mid+1,R);
PushUp(x);
}
inline void Set(int x,int L,int R,int pos,Data k){
if(L==R) return tr[x].dat=k,void();
PushDown(x);
int mid=L+R>>1;
if(pos<=mid) Set(x<<1,L,mid,pos,k);
else Set(x<<1|1,mid+1,R,pos,k);
PushUp(x);
}
inline void Modify(int x,int L,int R,int l,int r,ll k){
if(l>r) return ;
if(l<=L&&R<=r) return Push(x,k);
PushDown(x);
int mid=L+R>>1;
if(l<=mid) Modify(x<<1,L,mid,l,r,k);
if(r>mid) Modify(x<<1|1,mid+1,R,l,r,k);
PushUp(x);
}
inline Data Query(int x,int L,int R,int l,int r){
if(l<=L&&R<=r) return tr[x].dat;
PushDown(x);
int mid=L+R>>1;
if(r<=mid) return Query(x<<1,L,mid,l,r);
else if(l>mid) return Query(x<<1|1,mid+1,R,l,r);
else return Query(x<<1,L,mid,l,r)|Query(x<<1|1,mid+1,R,l,r);
}
struct Seg{
int l,r,t;
Seg(){}
Seg(int _l,int _r,int _t){l=_l,r=_r,t=_t;}
friend inline bool operator <(Seg s,Seg t){return s.l<t.l;}
};
multiset<Seg> o;
inline auto Split(int pos){
if(pos>o.rbegin()->r) return o.end();
auto it=--o.upper_bound(Seg(pos,0,0));
if(it->l==pos) return it;
int l=it->l,r=it->r,t=it->t;
o.erase(it);
o.insert(Seg(l,pos-1,t));
return o.insert(Seg(pos,r,t));
}
inline void Assign(int l,int r,int t){
auto rt=Split(r+1),lt=Split(l);
for(auto it=lt;it!=rt;it++) Modify(1,0,n,0,it->t-1,-(it->r-it->l+1));
o.erase(lt,rt);
o.insert(Seg(l,r,t));
}
Data f[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++) cin>>cl[i]>>cr[i];
f[0].Init(),f[0][0]=0;
Build(1,0,n),Set(1,0,n,0,f[0]);
o.insert(Seg(*min_element(cl+1,cl+n+1),*max_element(cr+1,cr+n+1),0));
for(int i=1;i<=n;i++){
Assign(cl[i],cr[i],i);
Modify(1,0,n,0,i,cr[i]-cl[i]+1);
f[i]=Query(1,0,n,0,i-1);
f[i]<<=1;
Set(1,0,n,i,f[i]);
}
for(int i=1;i<=k;i++) cout<<f[n][i]<<' ';cout<<endl;
return 0;
}
[NHSPC 2025] 资料中心
考虑对时间轴线段树分治。
在每个区间节点处的连通性是可以确定的,因此想到在每个节点处找出所有可能在子结点处产生贡献的边并下放,同时对一定最优的边进行缩点。根据颜色段均摊理论,每个边能作用的连续段总和是 \(O(n)\) 的,因此时间复杂度为 \(O(n\alpha(n)\log d)\)。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll=long long;
const int N=1e5+9;
const int M=3e5+9;
int n,m,d;
struct Edge{
int u,v,w,l,r;
};
struct DSU{
int fa[N],siz[N];
vector<int> recall;
inline void Init(){for(int i=1;i<=n;i++) fa[i]=i,siz[i]=1;}
inline int Find(int x){return fa[x]==x?x:fa[x]=Find(fa[x]);}
inline bool Merge(int x,int y){
x=Find(x),y=Find(y);
if(x==y) return 0;
if(siz[x]<siz[y]) swap(x,y);
fa[y]=x;
siz[x]+=siz[y];
recall.push_back(y);
return 1;
}
inline void Recall(){
for(int x:recall) fa[x]=x,siz[x]=1;
recall.clear();
}
}D1,D2;
inline void Conquer(int l,int r,int lft,ll ans,vector<Edge> e){
vector<Edge> tmp;
for(Edge &c:e){
if(c.r<l||c.l>r) continue ;
tmp.push_back(c);
}
e.swap(tmp);
for(Edge c:e) if(c.l>l||c.r<r) D1.Merge(c.u,c.v);
for(Edge c:e){
if(c.l<=l&&r<=c.r&&D1.Merge(c.u,c.v)){
D2.Merge(c.u,c.v);
ans+=c.w;
lft--;
}
}
D1.Recall();
tmp.clear();
for(Edge c:e){
c.u=D2.Find(c.u);
c.v=D2.Find(c.v);
if(l<c.l||c.r<r) tmp.push_back(c);
else if(D1.Merge(c.u,c.v)) tmp.push_back(c);
}
D1.Recall();
D2.Recall();
if(l==r) cout<<(lft?-1:ans)<<' ';
else{
int mid=l+r>>1;
Conquer(l,mid,lft,ans,tmp);
Conquer(mid+1,r,lft,ans,tmp);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>d;
vector<Edge> e(m);
for(Edge &c:e) cin>>c.u>>c.v>>c.w>>c.l>>c.r,c.r--;
sort(e.begin(),e.end(),[](Edge i,Edge j){return i.w<j.w;});
D1.Init(),D2.Init();
Conquer(0,d-1,n-1,0,e);
cout<<endl;
return 0;
}
[ICPC EC Online 2025 I] J. Moving on the Plane
显然旋转 \(45^\circ\),那么横轴和纵轴就独立了,同时曼哈顿距离转切比雪夫距离。可以预处理组合数,再容斥计算出每种距离的方案数,时间复杂度 \(O(nm)\)。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll=long long;
const int N=5e1+9;
const int M=1e5+9;
const int mod=998244353;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ll*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ll*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int fac[M],ifac[M];
inline void Init(int lim){
fac[0]=1;
for(int i=1;i<=lim;i++) fac[i]=Mul(fac[i-1],i);
ifac[lim]=Inv(fac[lim]);
for(int i=lim-1;~i;i--) ifac[i]=Mul(ifac[i+1],i+1);
}
inline int C(int n,int m){
if(m<0||m>n) return 0;
else return Mul(fac[n],Mul(ifac[m],ifac[n-m]));
}
int x[N],y[N],s[M],n,m,k;
inline int S(int l,int r){
l=max(0,m+l+1>>1);
r=min(m,m+r>>1);
if(l>r) return 0;
else return l?Sub(s[r],s[l-1]):s[r];
}
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>m>>k;
for(int i=1,p,q;i<=n;i++){
cin>>p>>q;
x[i]=p-q;
y[i]=p+q;
}
Init(m);
s[0]=C(m,0);
for(int i=1;i<=m;i++) s[i]=Add(s[i-1],C(m,i));
int ans=1;
for(int *a:{x,y}){
int res=0,L=*min_element(a+1,a+n+1),R=*max_element(a+1,a+n+1);
for(int l=L-m;l<=R+m;l++){
int r=l+k,m0=1,m1=1;
for(int i=1;i<=n;i++){
MulAs(m0,S(l-a[i],r-a[i]));
MulAs(m1,S((l+1)-a[i],r-a[i]));
}
AddAs(res,Sub(m0,m1));
}
MulAs(ans,res);
}
cout<<ans<<endl;
return 0;
}
[EC Final 2025] E. Efficient Express
首先 \(a_i\) 相同的限制一定被该 \(a_i\) 最左点 \(l_{a_i}\) 和最右点 \(r_{a_i}\) 的限制支配,所以有用的限制 \((l_i,r_i)\) 只有 \(O(n)\) 个。
先除去 \(\exists j,r_i\leq p_j\wedge a_{l_i}\geq x_i\) 这类已经满足的限制,那么对于剩下的限制 \(i\),必然存在路线 \(j\) 满足以下两者之一:
- \(p_j<l_i\wedge y_j\leq a_{l_i}\)。
- \(x_j\leq a_{l_i}\wedge y_j\leq a_{l_i}\)。
换言之,绘制出所有 \((l_i,a_{l_i})\) 和 \((p_j,x_j)\) 后,每个限制 \(i\) 的左边或下面必须存在 \(y_j\leq a_{l_i}\) 的路线 \(j\)。
由于对于下面的限制越向下越严,因此可以设 \(f_{i,j,t,0/1}\) 表示从左向右考虑,当前考虑到横坐标第 \(i\) 小的点,路线的最小 \(y\) 值为 \(j\),在左侧没被满足的限制向下要求的最紧的限制为存在 \(y\leq t\),以及该限制是否已经被满足。朴素转移时间复杂度 \(O((n+m)k^3)\),使用前缀和优化 DP 即可做到 \(O((n+m)k^2)\)。
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll=long long;
const int N=1e3+9;
const int K=5e2+9;
const int mod=998244353;
inline void AddAs(int &x,int y){if((x+=y)>=mod) x-=mod;}
inline void SubAs(int &x,int y){if((x-=y)<0) x+=mod;}
inline void MulAs(int &x,int y){x=1ll*x*y%mod;}
inline int Add(int x,int y){if((x+=y)>=mod) x-=mod;return x;}
inline int Sub(int x,int y){if((x-=y)<0) x+=mod;return x;}
inline int Mul(int x,int y){return 1ll*x*y%mod;}
inline int QPow(int x,int y){
int res=1;
while(y){
if(y&1) MulAs(res,x);
MulAs(x,x);
y>>=1;
}
return res;
}
inline int Inv(int x){return QPow(x,mod-2);}
int f[K][K],g[K][K],a[N],p[N],x[N],L[K],R[K],n,m,k;
signed main(){
cin.tie(0),cout.tie(0);
ios::sync_with_stdio(0);
cin>>n>>m>>k;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=m;i++) cin>>p[i]>>x[i];
vector<array<int,3>> o;
for(int i=1;i<=k;i++) L[i]=n+1,R[i]=0;
for(int i=1;i<=n;i++) R[a[i]]=i;
for(int i=n;i>=1;i--) L[a[i]]=i;
for(int i=1;i<=m;i++) o.push_back({p[i],x[i],0});
for(int i=1;i<=k;i++){
if(L[i]>=R[i]) continue ;
bool flag=0;
for(int j=1;j<=m;j++){
flag|=(R[i]<=p[j])&&(x[j]<=i);
}
if(!flag) o.push_back({L[i],i,1});
}
sort(o.begin(),o.end(),[](auto p,auto q){
if(p[0]!=q[0]) return p[0]<q[0];
else if(p[2]!=q[2]) return p[2]>q[2];
else return p[1]>q[1];
});
f[k+1][k+1]=1;
for(auto p:o){
if(!p[2]){
for(int j=1;j<=k+1;j++){
auto Upd=[](int j,int l,int r,int k){
if(l>r) return ;
AddAs(g[l][j],k);
SubAs(g[r+1][j],k);
};
for(int i=1;i<=k+1;i++){
if(!f[i][j]) continue ;
if(p[1]<=j){
Upd(k+1,1,min(i,j),f[i][j]);
Upd(k+1,i,i,Mul(f[i][j],max(j-i,0)));
Upd(j,j+1,i,f[i][j]);
Upd(j,i,i,Mul(f[i][j],Sub(k,max(i,j))));
}else{
Upd(j,1,i-1,f[i][j]);
Upd(j,i,i,Mul(f[i][j],k-i+1));
}
}
}
for(int j=1;j<=k+1;j++) for(int i=1;i<=k+1;i++) AddAs(g[i][j],g[i-1][j]);
}else{
for(int i=1;i<=k+1;i++){
for(int j=1;j<=k+1;j++){
if(!f[i][j]) continue ;
if(i<=p[1]) AddAs(g[i][j],f[i][j]);
else AddAs(g[i][min(j,p[1])],f[i][j]);
}
}
}
memcpy(f,g,sizeof f);
memset(g,0,sizeof g);
}
int ans=0;
for(int i=1;i<=k+1;i++) AddAs(ans,f[i][k+1]);
cout<<ans<<endl;
return 0;
}

浙公网安备 33010602011771号