2023牛客暑期多校训练营3 BE「计数dp」「支配树」

B-Auspiciousness

题意:

有2n张牌,一次操作为:

  1. 从堆顶拿一张牌
  2. 如果牌堆空了直接去步骤3。否则猜测下一张牌是否比本牌大,猜对继续,猜错去步骤3。
  3. 结束操作。

现在你的策略是,如果本张牌是1~n就猜下张大,否则猜小。询问2n张牌的所有排列,使用该策略,总共能摸到多少张牌,答案对m取模。
注意:就算没有猜对,拿到的牌也不会放回去

思路:

从1到n的数叫小数,以0表示,n+1到2n的数叫大数,用1表示。
由于每次最后一张牌永远能拿到,所以答案先加一个2n的阶乘(即所有排列方式)。
注意到,持续摸牌的阶段,摸到的牌一定是小数递增序列和大数递减序列交替
dp[ i ][ j ][ 0/1 ]表示拿了i张小数,j张大数,当前位置是小数递增/大数递减。
转移时枚举交替位置,小数由大数转移而来,枚举连续的小数k,由于连续的k个牌必须是递增的,从n-(i+k)个剩余可选的数里选k个,必须有序应用组合数
d p [ i ] [ j ] [ 0 ] = d p [ i − k ] [ j ] [ 1 ] ∗ C ( n − i + k , k ) dp[ i ][ j ][ 0 ] = dp[ i - k ][ j ][ 1 ]*C(n-i+k,k) dp[i][j][0]=dp[ik][j][1]C(ni+k,k)
在一轮dp中,我们的i+j是确定的,在dp时计算答案,剩余未摸的牌摸不到,所以怎么排序都可以,求一个全排列即可。
注意m不一定为质数,所以求组合数的时候应用杨辉三角求,不能用逆元。

#include <bits/stdc++.h>
using namespace std;
#define io ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
typedef long long ll;
#define int ll
#define pb push_back
#define eb emplace_back
#define m_p make_pair
const int mod = 998244353;
#define mem(a,b) memset(a,b,sizeof a)
#define pii pair<int,int>
#define fi first
#define se second
const int inf = 0x3f3f3f3f;
const int N = 3050;
//__builtin_ctzll(x);后导0的个数
//__builtin_popcount计算二进制中1的个数
int C[N][N];
int dp[N][N][2];
int fac[N];
int n,m;

void init(){
    fac[0]=1;
    for(int i=1;i<=2*n;++i){
        fac[i]=fac[i-1]*i%m;
    }
    for(int i=0;i<=2*n;++i){
        C[i][0]=C[i][i]=1;
        if(i==0)continue;
        for(ll j=1;j<=i-1;j++) {
            C[i][j]=(C[i-1][j]+C[i-1][j-1])%m;
        }
    }
    for(int i=0;i<=n;++i){
        for(int j=0;j<=n;++j){
            dp[i][j][0]=dp[i][j][1]=0;
        }
    }
}

void work() {
    cin>>n>>m;
    init();
    ll ans=fac[2*n]%m;
    dp[0][0][0]=dp[0][0][1]=1;//大小数都不选的情况只有1个
    for(int i=0;i<=n;++i){
        for(int j=0;j<=n;++j){
            for(int k=1;k<=i;++k){//选了i-k个小数,j个大数,从大数到小数转移
                dp[i][j][0]=(dp[i][j][0]+dp[i-k][j][1]*C[n-i+k][k])%m;
            }
            for(int k=1;k<=j;++k){//选了j-k个小数,i个大数,从小数到大数转移
                dp[i][j][1]=(dp[i][j][1]+dp[i][j-k][0]*C[n-j+k][k])%m;
            }
            if((i==0&&j==0)||(i==n&&j==n))continue;//当全没拿或者全拿完时都无法更新答案
            ans=(ans+(dp[i][j][0]+dp[i][j][1])*fac[2*n-i-j])%m;
        }
    }
    cout<<ans<<'\n';
}

signed main() {
    io;
    int t=1;
    cin >> t;
    while (t--) {
        work();
    }
    return 0;
}

E-Koraidon, Miraidon and DFS Shortest Path

题意:

给定一个有向联通图,询问该图是否满足从1开始无论怎样dfs,每个点获得的dis都始终是一个

思路:

支配树

钦定s为入口,若y为x的支配点,那么从s走到x的每一条路径都会经过y,即y是x的必经之路。

求支配点

朴素方法为O(n^3)dfs,试删去每个点查看其余哪个点不再能到达,即求得支配点。

进阶方法为O(n^2)数据流迭代法,一个点的支配点的点集为它所有父结点的支配点集的交集和它本身,根据该结论将每个结点上的支配点集不断迭代直至答案不变即可。为了提高效率,我们希望每轮迭代时,当前迭代的结点的所有父结点尽可能都已经执行完了这次迭代,因此我们要用拓扑序求。

DAG上的特例

由上述进阶法结论可知,u的支配点一定是其所有父结点在支配树上的公共祖先,那么显然u的直接支配点是所有父结点在支配树上的 LCA

在本题中,我们可以把1作为起始点,bfs出每个点的最小深度,bfs时忽略所有的返祖边横向边,得到一个DAG分层图,bfs出队顺序即为分层图拓扑序(但不是原图拓扑序),顺便lca一下,得到每个点的支配点集的路径,再dfs检验:

,对于每一条有向边连接的两点( u → v u \to v uv)有三种情况:

  • dep[u]=dep[v],说明该边是一条横向边,从1走到uv的路显然不是一条,那么走u的路去v,v将额外获得一个dep[u]+1的深度,显然dfs会出现不同结果。
  • dep[u]<dep[v],至少在u点中,v不会出现问题。
  • dep[u]>dep[v],说明该边是一条返祖边,如果v是u的支配点,说明出现有向环,走到u再走到v必经过过v,成立;如果v不是u的支配点,当从不经过v走到u的路径走到v时,v会分别获得一个大于u一个小于u的深度,显然错误。
AC代码
#include <bits/stdc++.h>
using namespace std;
#define io ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
typedef long long ll;
//#define int ll
#define pb push_back
#define eb emplace_back
#define m_p make_pair
const int mod = 998244353;
#define mem(a,b) memset(a,b,sizeof a)
#define pii pair<int,int>
#define fi first
#define se second
const int inf = 0x3f3f3f3f;
const int N = 5e5 + 50;
//__builtin_ctzll(x);后导0的个数
//__builtin_popcount计算二进制中1的个数
vector<int>e[N];
int dep1[N],dep2[N];//dep1原图深度,dep2分层图深度
int f[N][20];//倍增支配点
vector<int>fa[N];//fa[i][j],j是i的一个父亲
int n,m;
bool vis[N],ok=1;

int lca(int u,int v){//v>u
    if(dep2[u]>dep2[v]){
        swap(u,v);
    }
    while (dep2[v] > dep2[u]) {
        v = f[v][__lg(dep2[v] - dep2[u])];
    }
    if(u==v)return u;
    for(int i=19;i>=0;--i){
        if(f[u][i]!=f[v][i]){
            u=f[u][i];
            v=f[v][i];
        }
    }return f[u][0];
}

void bfs(){
    bool ok=0;
    queue<int>q;
    q.push(1);dep1[1]=dep2[1]=1;
    while(!q.empty()){
        int t=q.front();q.pop();
        if(t!=1){
            f[t][0]=fa[t][0];
            for(auto x:fa[t]){
                f[t][0]=lca(f[t][0],x);
            }
            dep2[t]=dep2[f[t][0]]+1;
            for (int i=1;i<20;++i) {
                f[t][i] = f[f[t][i - 1]][i - 1];
            }
        }

        for(auto x:e[t]){
            if(dep1[x]==0||dep1[x]>dep1[t]){//未访问或者dep2[x]-1=dep2[t]即下一层,成(非有向)环。
                fa[x].pb(t);
            }if(!dep1[x]){
                dep1[x]=dep1[t]+1;
                q.push(x);
            }
        }
    }
}

void dfs(int t){
    vis[t]=1;
    for(auto x:e[t]){
        if(dep1[x]==dep1[t]){//横向边,说明x点存在至少两种深度
            ok=0;
        }else if(dep1[x]>dep1[t]){//x在t的下层
            if(!vis[x]) dfs(x);
        }else{//x在t的上层
            if(lca(x,t)!=x){//如果x不是t的一个支配点,说明x存在两种可被遍历的方式,深度分别大于和小于t的深度
                ok=0;
            }
        }
    }
}

void work() {
    cin>>n>>m;
    ok=1;
    for(int i=1;i<=n;++i){
        e[i].clear();
        fa[i].clear();
        dep1[i]=dep2[i]=vis[i]=0;
        for(int j=0;j<20;++j){
            f[i][j]=0;
        }
    }

    for(int i=1;i<=m;++i){
        int u,v;cin>>u>>v;
        if(u==v)continue;
        e[u].pb(v);
    }
    bfs();dfs(1);
    if(ok)cout<<"Yes\n";
    else cout<<"No\n";
}

signed main() {
    io;
    int t=1;
    cin >> t;
    while (t--) {
        work();
    }
    return 0;
}

只会DAG不会别的支配树QaQ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值