2025.10~11 集训做题杂记
[CTS2024] 众生之门
小清新构造题。
观察大样例可以发现答案不大于 \(3\),感性猜测可以在路径长度不超过 \(3\) 的情况下遍历整棵树,事实也确实如此。
进一步考虑答案一般为 \(0\) 和 \(1\),只有 \(n\) 比较小或者图为菊花时答案是固定的。
所以我们对较小的 \(n\) 跑暴力,菊花图特判,对其余情况随机一个排列,每次交换两项,直到答案小于等于 \(1\) 输出即可。
由于答案值域是 \(O(n)\) 的,所以期望也是 \(O(n)\) 次找到答案,复杂度没问题。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=5e4+5;
int T,n,s,t,dep[N],p[N],ANS,ans[N];
int siz[N],wc[N],top[N],fa[N];
vector<int>e[N];
void dfs1(int now,int f){
siz[now]=1,wc[now]=0,fa[now]=f;
for(auto it:e[now]){
if(it!=f){
dep[it]=dep[now]+1;
dfs1(it,now);
siz[now]+=siz[it];
if(siz[it]>siz[wc[now]]) wc[now]=it;
}
}
}
void dfs2(int now,int Top){
top[now]=Top;
if(wc[now]) dfs2(wc[now],Top);
for(auto it:e[now]){
if(it!=fa[now]&&it!=wc[now]) dfs2(it,it);
}
}
int LCA(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
return dep[x]<dep[y]?x:y;
}
bool flower(){
for(int i=1;i<=n;i++) if(e[i].size()==n-1) return 1;
return 0;
}
int dis(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];}
mt19937 rd(std::random_device{}());
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>T;
while(T--){
cin>>n>>s>>t;
for(int i=1;i<=n;i++) e[i].clear();
for(int i=1,u,v;i<n;i++){
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dep[1]=0;dfs1(1,1),dfs2(1,1);
if(n<=8){
ANS=10;
for(int i=1;i<=n;i++) p[i]=i;
do{
if(p[1]!=s||p[n]!=t) continue;
int tmp=0;
for(int i=2;i<=n;i++) tmp^=dis(p[i],p[i-1]);
if(tmp<ANS){
ANS=tmp;
for(int i=1;i<=n;i++) ans[i]=p[i];
}
}while(next_permutation(p+1,p+n+1));
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
cout<<"\n";
}else{
p[1]=s,p[n]=t;
// for(int i=2;i<=n-1;i++) ans[i]=p[i]=i-1+(i-1>=s)+(i-1+(i-1>=s)>=t);
for(int i=1;i<=n;i++)if(i!=s&&i!=t) p[i-(i>s)-(i>t)+1]=i;
if(flower()){
for(int i=1;i<=n;i++) cout<<p[i]<<" ";
cout<<"\n";
continue;
}
int tmp=0;
for(int i=2;i<=n;i++) tmp^=dis(p[i],p[i-1]);
while(tmp>1){
int l=rd()%(n-2)+2,r=rd()%(n-2)+2;
while(l==r) r=rd()%(n-2)+2;
tmp^=dis(p[l],p[l-1])^dis(p[l],p[l+1])^dis(p[r],p[r-1])^dis(p[r],p[r+1]);
swap(p[l],p[r]);
tmp^=dis(p[l],p[l-1])^dis(p[l],p[l+1])^dis(p[r],p[r-1])^dis(p[r],p[r+1]);
}
for(int i=1;i<=n;i++) cout<<p[i]<<" ";
cout<<"\n";
}
}
}
P10813 【MX-S2-T4】 换
对于这种排序网络的题,可以考虑一个经典 trick,将序列 \(\{A_n\}\) 重赋值为 \(\{B_n(V)\}\) 满足:\(B_{i}(V)=[A_i\le V]\)。
一个序列 \(\{A_n\}\) 合法,当且仅当 \(\forall V,\{B_n(V)\}\) 合法。
考虑直接预处理出所有 01 序列是否合法,这是 \(O(m2^n)\) 的。
发现序列 \(A\) 离散化后只有 \(O(n)\) 个 \(V\) 是有意义的,考虑状压 DP,记 \(dp_{i,S}\) 表示离散化后 \(\{B_n(i)\}=S\) 且 \(\exists x,A_x=i\) 的方案数。
转移就是填入 \(i+1\),有 \(dp_{i+1,S}=\sum_{T\subset S}dp_{i,T}\)。
直接上子集枚举是 \(O(n3^n)\) 的,不太可行,考虑每一层 DP 前做一遍高维前缀和,于是复杂度变为 \(O(n^22^n)\)。
总时间复杂度 \(O(n^2+m)2^n\)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=18,M=505,mod=1e9+7;
ll n,V,m,dp[1<<N],SS[1<<N],ans;
int x[M],y[M];
bool f[1<<N],p[N+5];
bool check(int S){
for(int i=1;i<=n;i++) p[i]=(S>>(i-1))&1;
for(int i=1;i<=m;i++){
if(p[x[i]]>p[y[i]]) swap(p[x[i]],p[y[i]]);
}
bool tmp=1;
for(int i=1;i<n;i++) tmp&=(p[i]<=p[i+1]);
return tmp;
}
ll q_pow(ll x,ll b){
ll c=1;
while(b){
if(b&1) c=c*x%mod;
x=x*x%mod,b>>=1;
}
return c;
}
ll inv(ll x){return q_pow(x,mod-2);}
ll C(ll x,ll y){
if(x<y||y<0)return 0;
ll ret=1;
for(int i=1;i<=y;i++) ret=ret*(x-i+1)%mod*inv(i)%mod;
return ret;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>V>>m;
for(int i=1;i<=m;i++) cin>>x[i]>>y[i];
for(int S=0;S<(1<<n);S++) f[S]=check(S);
dp[0]=1;
for(int i=1;i<=n;i++){
for(int S=0;S<(1<<n);S++) SS[S]=dp[S];
for(int j=0;j<n;j++){
for(int S=0;S<(1<<n);S++){
if(S&(1<<j)) SS[S]=(SS[S]+SS[S^(1<<j)])%mod;
}
}
for(int S=0;S<(1<<n);S++) dp[S]=(SS[S]-dp[S]+mod)*f[S]%mod;
ans=(ans+C(V,i)*dp[(1<<n)-1]%mod)%mod;
}
cout<<ans<<"\n";
}
The only survival
发现每次可以钦定一堆点满足 \(dis_{1,n}\in [1,k] \cup \{> k\}\),这里 \(>k\) 当作同一类点,因为对 \(dis_{1,n}=k\) 没有意义。同时 \(dis_{1,n}\) 必须钦定为 \(k\)。
新加入一个点 \(v\) 时,对于一条边 \(<u,v,w>\) 有 \(w\in[dis_{1,v}-dis_{1,u},lim]\) 乘法原理即可算出总方案数,同时注意必须存在一条边满足 \(w=dis_{1,v}-dis_{1,u}\) ,减掉 \(\forall <u,v,w>,w>dis_{1,v}-dis_{1,u}\) 的方案数即可,快速求方案数可以记录 \(cnt_i=\sum_x [dis_{1,x}=i]\)。
时间复杂度是划分数,所以 \(n\le 13\) 能跑。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=15;
ll n,lim,k,mod,cnt[N],ans;
ll q_pow(ll x,ll b){
ll c=1;
while(b){
if(b&1) c=c*x%mod;
x=x*x%mod,b>>=1;
}
return c;
}
ll inv(ll x){return q_pow(x,mod-2);}
ll fac[N],C[N][N];
void init(int x){fac[0]=1;for(int i=1;i<=x;i++) fac[i]=fac[i-1]*i%mod;}
void dfs(int dep,int num,ll tmp){
if(dep>k){
ll tmpv=1;
for(int i=0;i<=k;i++) tmpv=tmpv*q_pow(lim-(k-i),cnt[i])%mod;
return ans=(ans+tmp*q_pow(tmpv,n-num)%mod*q_pow(lim,(n-num)*(n-num-1)/2)%mod)%mod,void();
}else{
ll tmpv=1,subv=1;
for(int i=0;i<dep;i++) tmpv=tmpv*q_pow(lim-(dep-i)+1,cnt[i])%mod,subv=subv*q_pow(lim-(dep-i),cnt[i])%mod;
tmpv=(tmpv+mod-subv)%mod;
for(int i=(dep==k);num+i<=n;i++) cnt[dep]=i,dfs(dep+1,num+i,tmp*q_pow(tmpv,i)%mod*q_pow(lim,i*(i-1)/2)%mod*C[n-num-1][i-(dep==k)]%mod);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
freopen("graph.in","r",stdin);
freopen("graph.out","w",stdout);
cin>>n>>k>>lim>>mod;
if(lim<k) return cout<<"0\n",0;
C[0][0]=1;
for(int i=1;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++) C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
init(n);cnt[0]=1,n--;
dfs(1,0,1);
cout<<ans<<"\n";
}
神秘式子一则
原题钦定顺序然后就是求将 \(n\) 个不同小球放进任意个相同盒子使得每个盒子小球数量 \(\ge2\) 的方案数。
每个盒子至少放一个小球方案数是简单的斯特林数,考虑对只有一个小球的盒子个数容斥。
于是有:
考虑拆开斯特林数:
后半是个前缀和的形式,可以预处理出来,于是复杂度 \(O(n)\)。
TEST_90
从矩阵乘法的形式来理解区间历史和问题,继而可以推广到任意双半群模型,算是个经典 trick 了。
排序后扫描线,转换为历史和问题是平凡的。
设线段树节点维护 \(\{S,PS,L\}\),代表当前区间和,历史和,区间长度,考虑两个 tag 所对应的矩阵。
Xor:
Tim:
直接做就行,但你发现直接矩乘是过不去的。
这个里面说的很好:
矩阵乘法更多是用来理解标记的,而不是用在代码实现上的。
发现矩阵中只有四个值会变化,其余值都是固定的,所以只维护这四个值就可以。
struct M{
ll d[4];
ll& operator[](size_t x){return d[x];}
friend M operator*(M x,M y){
return {x[0]*y[1]+y[0],x[1]*y[1],x[2]+x[1]*y[2],x[3]+y[3]+x[0]*y[2]};
}
};
const M I={0,1,0,0};
M xort(){return {1,-1,0,0};}
M tim(){return {0,1,1,0};}
其中 \(d_0: L\rightarrow S,d_1: S\rightarrow S,d_2: S\rightarrow PS,d_3: L\rightarrow PS\)。
然后矩阵乘法常数就很小了。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e6+5;
int n,d,m,a[N],l[N],lst[N];
ll ans[N];
vector<PII>q[N];
struct M{
ll d[4];
ll& operator[](size_t x){return d[x];}
friend M operator*(M x,M y){
return {x[0]*y[1]+y[0],x[1]*y[1],x[2]+x[1]*y[2],x[3]+y[3]+x[0]*y[2]};
}
};
const M I={0,1,0,0};
M xort(){return {1,-1,0,0};}
M tim(){return {0,1,1,0};}
struct Segment_Tree{
struct Segment_Tree_node{
ll S,PS,L;M tag;
friend Segment_Tree_node operator+(Segment_Tree_node x,Segment_Tree_node y){return {x.S+y.S,x.PS+y.PS,x.L+y.L,I};}
friend Segment_Tree_node operator*(Segment_Tree_node x,M y){return {x.S*y[1]+x.L*y[0],x.PS+x.S*y[2]+x.L*y[3],x.L,x.tag*y};}
}t[N<<2];
inline int ls(int x){return x<<1;}
inline int rs(int x){return x<<1|1;}
#define mid ((l+r)>>1)
inline void f(int p,M x){t[p]=t[p]*x;}
inline void push_up(int p){t[p]=t[ls(p)]+t[rs(p)];}
inline void push_down(int p){f(ls(p),t[p].tag),f(rs(p),t[p].tag),t[p].tag=I;}
void build(int p,int l,int r){
if(l==r) return t[p]={0,0,1,I},void();
else{
build(ls(p),l,mid),build(rs(p),mid+1,r);
push_up(p);
}
}
void change(int p,int l,int r,int re_l,int re_r,int op){
if(re_l<=l&&r<=re_r) return f(p,op?tim():xort());
else{
push_down(p);
if(re_l<=mid) change(ls(p),l,mid,re_l,re_r,op);
if(mid<re_r) change(rs(p),mid+1,r,re_l,re_r,op);
push_up(p);
}
}
ll query(int p,int l,int r,int re_l,int re_r){
if(re_l<=l&&r<=re_r) return t[p].PS;
else if(!(r<re_l||l>re_r)) return push_down(p),query(ls(p),l,mid,re_l,re_r)+query(rs(p),mid+1,r,re_l,re_r);
else return 0;
}
}T;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
cin>>m;
for(int i=1,l,r;i<=m;i++) cin>>l>>r,q[r].push_back({l,i});
T.build(1,1,n);
for(int i=1;i<=n;i++){
T.change(1,1,n,lst[a[i]]+1,i,0);
T.change(1,1,n,1,i,1);
for(auto it:q[i]) ans[it.se]=T.query(1,1,n,it.fi,i);
lst[a[i]]=i;
}
for(int i=1;i<=m;i++) cout<<ans[i]<<"\n";
}
CF995F Cowmpany Cowmpensation
比较好计数题。
容易想到一个树形 DP,记 \(dp_{i,j}\) 表示考虑完点 \(i\) 的子树,点 \(i\) 填 \(j\) 的方案数,于是有:
这个是容易前缀和优化的,但考虑到点 \(1\) 填 \(j\),整棵树树内却不一定能覆盖完 \([1,j]\),直接统计答案会算重,考虑设 \(g_i\) 表示填数恰好覆盖 \([1,i]\) 的方案数,有:
系数 \(\binom{i-1}{j-1}\) 是因为 \(g_j\) 中点 \(1\) 填的值已经确定,所以剩下的点只需要从 \(i-1\) 种颜色选 \(j-1\) 个。
答案即为:
整体可以做到 \(O(n^2)\)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=3005,mod=1e9+7;
template<int mod> struct Modint{......};//Modint 类
#define MI Modint<mod>
int n,D;
vector<int>e[N];
MI C[N][N],dp[N][N],S[N][N],g[N],ans;
MI binom(ll x,ll y){
MI ret=1;
for(ll i=1;i<=y;i++) ret=ret*(x-i+1)/i;
return ret;
}
void DP(int now,int fa){
for(int i=1;i<=n;i++) dp[now][i]=1;
for(auto it:e[now]){
DP(it,now);
for(int i=1;i<=n;i++) dp[now][i]*=S[it][i];
}
for(int i=1;i<=n;i++) S[now][i]=S[now][i-1]+dp[now][i];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>D;
for(int i=2,fa;i<=n;i++) cin>>fa,e[fa].push_back(i);
for(int i=0;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++) C[i][j]=C[i-1][j]+C[i-1][j-1];
}
DP(1,0);
g[1]=1;ans+=D;
for(int i=2;i<=min(n,D);i++){
g[i]=dp[1][i];
for(int j=1;j<i;j++) g[i]-=C[i-1][j-1]*g[j];
ans+=binom(D,i)*g[i];
}
cout<<ans<<"\n";
}
Tree Tweaking
很有意思的一道题。
给定一个长度为 \(n\) 的排列 \({A_n}\),按此顺序构建一棵二叉搜索树。
你可以重新排列排列中下标在 \([l, r]\) 范围内的键的插入顺序,其余键按原顺序插入。
目标是通过重排 \([l, r]\) 部分的插入顺序,使得最终 BST 的所有节点的深度之和最小,输出这个最小的深度和。
根节点深度为 1。
\(1\le n\le 10^5\),\(1\le l\le r\le n\),\(r-l\le200\)。
首先有 \(\sum dep=\sum siz\)。于是问题转换为子树大小之和最小。
考虑由于 \({A_n}\) 是一个排列,所以一个节点的子树大小必然等于插入这个节点时的 \(nxt-pre-1\),其中 \(nxt\) 为这个值的后继,\(pre\) 为这个值的前驱。
将插入的值拍到数轴上,因为当 \(x<l\) 或者 \(x>r\) 时,数轴上有值的位置时固定的,所以 \(siz_{[1,l-1]}\) 和 \(siz_{[r+1,n]}\) 是固定的。
于是可以修改的部分被前 \(l-1\) 个插入的数划分成了若干个连续段,对每一段分开考虑。
记 \(dp_{l,r}\) 表示这个连续段里插入所有段内第 \(l\) 到 \(r\) 个数的最小贡献,\(v_i\) 表示段内第 \(i\) 个数的值。
有 \(dp_{l,r}=v_{r+1}-v_{l-1}-1+\min \limits_{p=l}^r\{dp_{l,p-1}+dp_{p+1,r}\}\)。
一个段的答案即为 \(dp_{2,tp-1}\)。
具体见代码。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e5+5,M=205;
ll n,a[N],L,R,v[N],dp[M][M],ans;
vector<int>num[N];
set<int>se;
void add(int x){
auto nxt=se.upper_bound(x);
ans+=(*nxt)-(*(--nxt))-1;
se.insert(x);
}
ll DP(int l,int r){
int tp=0;
v[++tp]=l;sort(num[r].begin(),num[r].end());
for(auto it:num[r]) v[++tp]=it;
v[++tp]=r;
for(int i=1;i<=tp;i++){
for(int j=i;j<=tp;j++) dp[i][j]=0x3f3f3f3f;
}
for(int len=1;len<tp;len++){
for(int i=2;i+len-1<tp;i++){
int j=i+len-1;
for(int k=i;k<=j;k++) dp[i][j]=min(dp[i][j],v[j+1]-v[i-1]-1+dp[i][k-1]+dp[k+1][j]);
}
}
return dp[2][tp-1];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
se.insert(0),se.insert(n+1);
cin>>L>>R;
for(int i=1;i<L;i++) add(a[i]);
for(int i=L;i<=R;i++) num[*se.upper_bound(a[i])].push_back(a[i]);
for(auto it=se.begin();it!=se.end();it++){
if(num[*it].size()) ans+=DP(*prev(it),*it);
}
for(int i=L;i<=R;i++) se.insert(a[i]);
for(int i=R+1;i<=n;i++) add(a[i]);
cout<<ans<<"\n";
}
Chords
钦定代表元的 trick?
断环为链,考虑一个联通分量必然包含于唯一一个极小区间 \([l,r]\),那么钦定 \([l,r]\) 为这个联通分量的代表元,考虑对代表元计数。
观察一个代表元需要满足什么样的条件:
- \(l\) 和 \(r\) 联通。
- \([l,r]\) 与外部没有连边。
第二个条件是好判断的,枚举代表元的时候 check 一下出边就行。
强制 \(l\) 和 \(r\) 联通是一个区间完全覆盖问题,不太好办,考虑用总方案数减去不联通的方案数。
总方案数是简单的,即为 \([2\mid c(l,r)]*c(l,r)!!\),其中 \(c(l,r)\) 为 \([l,r]\) 没有被钦定连边的点数。
考虑减去不联通的方案数,枚举与 \(l\) 联通的最右边的点 \(k\) 即可。
记 \(f(x)=[2\mid x]*x!!\),于是:
统计答案是简单的,代表元内部组成方案乘以外部连边方案即可:
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=605,mod=1e9+7;
template<int mod> struct Modint{......};
#define MI Modint<mod>
int n,m,to[N],v[N][N];
MI dp[N][N],g[N],ans;
bool f[N];
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m;g[0]=1;n<<=1;
for (int i=2;i<=n;i+=2) g[i]=g[i-2]*(i-1);
for(int i=1,x,y;i<=m;i++) cin>>x>>y,f[x]=f[y]=1,to[x]=y,to[y]=x;
for(int l=1;l<=n;l++){
for(int r=l;r<=n;r++) v[l][r]=v[l][r-1]+(!f[r]);
}
for(int l=1;l<=n;l++){
for(int r=l+1;r<=n;r+=2){
bool ok=1;
for(int k=l;k<=r;k++) ok&=(!f[k]||(f[k]&&(to[k]>=l&&to[k]<=r)));
if(!ok) continue;
dp[l][r]=g[v[l][r]];
for(int k=l;k<r;k++) dp[l][r]-=dp[l][k]*g[v[k+1][r]];
ans+=dp[l][r]*g[n-2*m-v[l][r]];
}
}
cout<<ans<<"\n";
}
[SCOI2016] 萌萌哒
\(O(nm\alpha(n))\) 做法是简单的,考虑优化。
这种连边问题首先可能会往线段树优化建图的方面去想,但是两个区间在线段树上的划分方式可能不同,比如一个划分成长度为 \([1,4,1]\) 的三个区间,另外一个划分成 \([4,2]\),这个做法就破产了。
这时候有一个 trick 是倍增优化并查集,因为并查集满足结合律,所以考虑类似 ST 表的结构,我们可以把一个区间划分成 \(O(log n)\) 个长度为 \(2^k\) 的段,这时候划分方式就只与长度相关了,可以每段分别合并。
考虑统计答案,我们可以直接把第 \(k\) 层的并查集与 \(k-1\) 层的并查集合并,将 \([i,i+2^k)\) 拆成 \([i,i+2^{k-1}),[i+2^{k-1},i+2^k)\),由于是同层整个块一起合并,我们容易根据点 \(i\) 的合并对象推出点 \(i+2^{k-1}\) 的合并对象,然后在第 \(0\) 层查联通块数量。
答案就是 \(9\times 10^{num-1}\),\(num\) 即为联通块数量。
具体看代码,复杂度 \(O(n\log n\alpha(n))\)。
#include<bits/stdc++.h>
using namespace std;
#define PII pair<int,int>
#define PLI pair<ll,int>
#define PIL pair<int,ll>
#define PLL pair<ll,ll>
#define fi first
#define se second
#define YES() cout<<"YES\n",0
#define NO() cout<<"NO\n",0
#define Yes() cout<<"Yes\n",0
#define No() cout<<"No\n",0
using ll=long long;
using uint=unsigned int;
using ull=unsigned long long;
using lb=long double;
const int N=1e5+5,mod=1e9+7,LG=18;
template<int siz>struct DSU{
int fa[siz],sz[siz];
void clear(int x){for(int i=1;i<=x;i++) fa[i]=i,sz[i]=1;}
int find(int x){return (fa[x]==x?x:fa[x]=find(fa[x]));}
void merge(int x,int y){return same(x,y)?void():(sz[find(y)]+=sz[find(x)],fa[find(x)]=find(y),void());}
bool same(int x,int y){return find(x)==find(y);}
};
DSU<N>dsu[LG];
ll n,m,ans;
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin>>n>>m;
for(int i=0;i<LG;i++) dsu[i].clear(n);
for(int i=1,la,ra,lb,rb;i<=m;i++){
cin>>la>>ra>>lb>>rb;
for(int k=LG-1;k>=0;k--){
if(la+(1<<k)-1<=ra){
dsu[k].merge(la,lb);
la+=(1<<k),lb+=(1<<k);
}
}
}
for(int i=LG-1;i>0;i--){
for(int j=1;j+(1<<i)-1<=n;j++){
int p=dsu[i].find(j);
dsu[i-1].merge(j,p),dsu[i-1].merge(j+(1<<i-1),p+(1<<i-1));
}
}
for(int i=1;i<=n;i++){
if(dsu[0].find(i)==i) ans=(!ans?9:(ans*10%mod));
}
cout<<ans<<"\n";
}

浙公网安备 33010602011771号