【学习笔记】ARC160

先考虑在模 K K K意义下将所有位置变成 0 0 0,从前往后把序列扫一遍即可。然后如果此时所有位置都是非负数那么这个序列就是合法的。

考虑将操作 2 2 2在第 1 ∼ N − K + 1 1\sim N-K+1 1NK+1个位置上执行的次数给固定,记作 { B i } \{B_i\} {Bi},因为对于每个位置都与前 K K K个位置的操作次数有关,所以有等式 A i = ∑ j < K B i − j A_i=\sum_{j<K}B_{i-j} Ai=j<KBij,那么只要 M ≡ ∑ A i ( m o d K ) M\equiv \sum A_i\pmod K MAi(modK)就可以愉快的插板法计算了,又因为有解所以显然有 K ∣ M K|M KM,所以只需要 K ∣ ∑ A i K|\sum A_i KAi,这是显然成立的。所以问题转换为对于所有 { B i } \{B_i\} {Bi},对于 ( M K − ∑ B i + N − 1 N − 1 ) \binom{\frac{M}{K}-\sum B_i+N-1}{N-1} (N1KMBi+N1)求和,其中 B i ∈ [ 0 , K − 1 ] B_i\in [0,K-1] Bi[0,K1]

这个地方可以用生成函数来推导。当然这不是我所擅长的

我们要求 [ x M K ] ( 1 − x K 1 − x ) N − K + 1 ( 1 1 − x ) N [x^{\frac{M}{K}}](\frac{1-x^K}{1-x})^{N-K+1}(\frac{1}{1-x})^N [xKM](1x1xK)NK+1(1x1)N,这里涉及到多项式除法,当然我们只要记住最基本的结论: 1 1 − x = ∑ n = 0 ∞ x n \frac{1}{1-x}=\sum_{n=0}^\infty x^n 1x1=n=0xn,所以 ( 1 1 − x ) m = ∑ i ≥ 0 ( i + m − 1 m − 1 ) x i (\frac{1}{1-x})^m=\sum_{i\ge 0}\binom{i+m-1}{m-1}x^i (1x1)m=i0(m1i+m1)xi,暴力卷积即可。

#include<bits/stdc++.h> #define ll long long #define fi first #define se second #define pb push_back #define inf 0x3f3f3f3f using namespace std; const int mod=998244353; int n,K; ll m,res,fac[6005],inv[6005]; ll fpow(ll x,ll y=mod-2){ ll z(1); for(;y;y>>=1){ if(y&1)z=z*x%mod; x=x*x%mod; }return z; } ll binom(ll x,ll y){ if(x<0||y<0||x<y)return 0; ll res=1; for(int i=0;i<y;i++)res=res*((x-i)%mod)%mod; res=res*inv[y]%mod; return res; } void init(int n){ fac[0]=1;for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod; inv[n]=fpow(fac[n]); for(int i=n;i>=1;i--)inv[i-1]=inv[i]*i%mod; } int main(){ ios::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>n>>m>>K; init(2*n-K); if(m%K){ cout<<0; return 0; } for(int i=0;i<=n-K+1;i++){ if(i&1)res=(res-binom(n-K+1,i)*binom(m/K-K*i+2*n-K,2*n-K))%mod; else res=(res+binom(n-K+1,i)*binom(m/K-K*i+2*n-K,2*n-K))%mod; } cout<<(res+mod)%mod; }

这题的难度在于找出反例。换句话说,要厘清结论成立的前提条件。当然,也需要对点双相关性质的熟悉。

显然每个叶子节点至少要连一条边。设叶子节点的数目为 L L L,如果 L L L为偶数,那么我们思考能否将叶节点两两配对,使整颗树构成一个点双,这样答案就是叶节点的权值之和。

考虑两个点双之间的合并。设有两个点双 S , T S,T S,T,若 ∣ S ∩ T ∣ > 1 |S\cap T|>1 ST>1那么 S ∪ T S\cup T ST也构成一个点双。这个结论的证明是容易的,考虑任删一个点,从 S ∩ T S\cap T ST中任选一个节点 v v v,显然从 v v v出发能到达 S ∪ T S\cup T ST中的所有节点,因此所有节点联通,那么 S ∪ T S\cup T ST构成一个点双。

那么,在给定的树上,最简单的点双就是一个环,如果两个环只有一个交点,那么就不能合并,反之就能合并。那么我们考虑将 v i v_i vi v i + L 2 v_{i+\frac{L}{2}} vi+2L配对,这样就合成一个大的点双了。

这样就完备了吗?事实上,上述构造在有些情况下是行不通的。这样的反例非常多。最简单的情况是根节点下面挂了很多条链,这样所有环都只交于一个点,就不能合并在一起。可以想象当单链的数目非常多的时候也是不合法的,因为两个单链连起来的点双和外界最多只有一个公共点。

把这一点想清楚后,我们就能理解这道题为什么要限制度数 ≤ 3 \le 3 3了。如果 L L L为偶数那么直接构造即可,如果 L L L为奇数那么把权值最小的点 v v v找出来,这样构造不合法当且仅当叶子节点数目为 3 3 3,并且以 v v v为根时满足 L C A ( x , y ) = L C A ( x , z ) = L C A ( y , z ) = v LCA(x,y)=LCA(x,z)=LCA(y,z)=v LCA(x,y)=LCA(x,z)=LCA(y,z)=v,简单判断一下即可。

代码就比较好写了。

石锤了。这题代码我调了好久。构造方案也有一些细节,总之挺恶心的。

代码是加了检验的版本。

复杂度 O ( n ) O(n) O(n)

#include<bits/stdc++.h> #define ll long long #define fi first #define se second #define pb push_back using namespace std; const int N=2e5+5; int T,n,degree[N],w[N],ban[N]; ll res; vector<int>G[N]; vector<int>vec; void dfs(int u,int topf){ if(degree[u]==1){ vec.pb(u); } for(auto v:G[u]){ if(v!=topf){ dfs(v,u); } } } int low[N],dfn[N],num,cnt; void tarjan(int u){ dfn[u]=low[u]=++num; for(auto v:G[u]){ if(!dfn[v]){ tarjan(v),low[u]=min(low[u],low[v]); if(low[v]>=dfn[u]){ cnt++; } } else{ low[u]=min(low[u],dfn[v]); } } } vector<pair<int,int>>edges; void dfs2(int u,int topf){ ban[u]=1; for(auto v:G[u]){ if(v!=topf&&degree[v]<=2){ dfs2(v,u); } } } void solve(){ cin>>n;for(int i=1;i<=n;i++)G[i].clear(),degree[i]=0,ban[i]=0; for(int i=1;i<=n;i++)cin>>w[i]; edges.clear(); for(int i=1;i<n;i++){ int u,v;cin>>u>>v; G[u].pb(v),G[v].pb(u); degree[u]++,degree[v]++; edges.pb({u,v}); } vec.clear(); int rt=1;while(degree[rt]==1)rt++; dfs(rt,0); cout<<(vec.size()+1)/2<<"\n"; if(vec.size()&1){ int p=-1,p2=-1; for(int i=1;i<=n;i++){ if(p==-1||w[p]>w[i])p2=p,p=i; else if(p2==-1||w[p2]>w[i])p2=i; } if(vec.size()==3&&degree[p]==3){ p=p2; } dfs2(p,0); int ok=0; for(int i=0;i<vec.size();i++){ if(!ban[vec[i]]){ cout<<p<<" "<<vec[i]<<"\n"; G[p].pb(vec[i]),G[vec[i]].pb(p); vec.erase(vec.begin()+i); ok=1; break; } } assert(ok); } for(int i=0;i<vec.size()/2;i++){ int u=vec[i],v=vec[i+vec.size()/2]; cout<<u<<" "<<v<<"\n"; G[u].pb(v),G[v].pb(u); } for(int i=1;i<=n;i++)dfn[i]=low[i]=0;num=0; cnt=0; tarjan(1); if(cnt>1){ for(auto x:edges){ cout<<x.fi<<" "<<x.se<<"\n"; } for(int i=1;i<=n;i++)cout<<w[i]<<" "; exit(0); } } int main(){ ios::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>T; while(T--){ solve(); } }

神仙题。我自己做是真看不出来正解。

考虑固定交换序列,然后对合法的排列计数。那么我们一定要利用类似哈希的思想将一个排列压缩成二进制串

接下来一步非常难想到。考虑对于任意 v ∈ [ 1 , n − 1 ] v\in [1,n-1] v[1,n1],将 ≤ v \le v v的位置看成 0 0 0 > v >v >v的位置看成 1 1 1,那么最终序列一定是一段 0 0 0和一段 1 1 1拼接起来。然后考虑将一个 0 0 0变成 1 1 1的时候,无论是 0 0 0还是 1 1 1这个数都一定会被交换,我们就可以直接预处理出交换过后的位置判断即可。这就非常机智了,感觉要在短时间内有这个想法确实很困难啊。

经验主义容易使人犯错。问题的突破口在 m m m上。称一个交换操作是合法的当且仅当会对最终序列造成影响。注意到是在交换序列的末尾添加操作,这提示我们去分析合法交换操作的次数。我们不妨来细致分析一下。

这个地方显然只需要分析单个排列的情形。假设交换 ( a , b ) (a,b) (a,b),那么 ( a , b ) (a,b) (a,b)一定变成不合法;如果 ( c , a ) (c,a) (c,a)变成合法,那么 ( c , b ) (c,b) (c,b)一定变成不合法,另一种情况是同理的,因此合法交换的数目是递减的,总的合法交换的数目不会超过 n 2 n^2 n2。之所以要在这里呈现,是因为这个结论的证明其实是极易的,因此考场上有精力一定有必要证一下,来验证自己的想法。

于是问题回到如何判断一个交换是否是合法的。我们完全有理由每次修改后全部预处理出来。沿用前面的思想处理即可。

复杂度 O ( 2 n n 3 ) O(2^nn^3) O(2nn3)

还是太菜了,改了半天才过。

#include<bits/stdc++.h> #define ll long long #define int ll #define fi first #define se second #define pb push_back #define inf 0x3f3f3f3f using namespace std; int n,m,num,valid[15][15],to[1<<15],exist[1<<15],state[15]; ll dp[1<<15],lastans; void add(ll &x,ll y){x+=y;} void solve(){ memset(dp,0,sizeof dp),dp[0]=1; memset(exist,0,sizeof exist),memset(state,0,sizeof state); for(int s=0;s<1<<n;s++){ int tot=__builtin_popcount(s),tmp=0; for(int i=0;i<tot;i++)tmp|=1<<n-i-1; if(dp[s]){ assert(to[s]==tmp); for(int i=0;i<n;i++){ if(!(s>>i&1)){ if(to[s|(1<<i)]==tmp+(1<<n-tot-1))add(dp[s|(1<<i)],dp[s]); } } } for(int i=0;i<n;i++){ if(!(s>>i&1)){ state[__builtin_ctz(to[s]^to[s|(1<<i)])]|=exist[s]; exist[s|(1<<i)]|=(to[s]^to[s|(1<<i)]); } } } for(int i=0;i<n;i++){ for(int j=i+1;j<n;j++){ valid[i][j]=state[j]>>i&1; } } } signed main(){ ios::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin>>n>>m; for(int i=0;i<n;i++){ for(int j=i+1;j<n;j++){ valid[i][j]=1; } } for(int i=0;i<1<<n;i++){ to[i]=i; } lastans=1; for(int i=1;i<=m;i++){ int x,y,l,r;cin>>x>>y; l=(x+lastans)%n+1; r=(y+lastans*2)%n+1; if(l>r)swap(l,r); l--,r--; if(valid[l][r]){ num++; assert(num<=n*n); for(int s=0;s<1<<n;s++){ if((to[s]>>l&1)&&!(to[s]>>r&1)){ to[s]^=1<<l,to[s]^=1<<r; } } solve(); } cout<<(lastans=dp[(1<<n)-1])<<"\n"; } }

__EOF__

本文作者仰望星空的蚂蚁
本文链接https://www.cnblogs.com/cqbzly/p/17529971.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   仰望星空的蚂蚁  阅读(10)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示