【学习笔记】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";
    }
}
posted @ 2023-05-25 15:12  仰望星空的蚂蚁  阅读(21)  评论(0)    收藏  举报  来源