bzoj1494 生成树计数(状压dp+生成树+矩阵倍增)

看数据,k<=5,n<=1e15,n极大,显然只能矩阵倍增去算。考虑dp,k极小,每个点最多能和前k个点连边,因此我们需要知道前k个点的联通情况,采用状压dp,用最小表示法。
我们可以先用一个DFS预处理出所有可能出现的连通性的状态。然后再枚举连通性状态S以及下一个点和S里的K个点中的哪些点连边,再判断从连通性状态S转移出来的新状态S′是否是合法的,若合法,在邻接矩阵里,标记从S到S′的方案数加1.

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define mod 65521
int const N=60;
int m,temp[7],sta[N][7],tot=0,ini[N],ans=0;
bool f[7],edge[7];//f[i]--i连通块是否已连过,edge[i]-i位置是否连边了 
ll n;
struct matrix{
    int mat[N][N];
    matrix(bool ff){
        memset(mat,0,sizeof(mat));
        if(ff) for(int i=1;i<=tot;++i) mat[i][i]=1;
    }
    matrix operator*(matrix b){
        matrix re(0);
        for(int i=1;i<=tot;++i)
            for(int j=1;j<=tot;++j)
                for(int k=1;k<=tot;++k)
                    re.mat[i][j]=(re.mat[i][j]+(ll)mat[i][k]*b.mat[k][j])%mod;
        return re;
    }
    matrix operator^(ll k){
        matrix re(1),base(0);
        memcpy(base.mat,mat,sizeof(mat));
        for(;k;k=k>>1,base=base*base)
            if(k&1) re=re*base;
        return re;
    }
}trans(0);
void dfs1(int i,int x){//前i-1个点分成了x个连通块 
    if(i==m+1){
        ++tot;
        for(int j=1;j<=m;++j) sta[tot][j]=temp[j];return;
    }
    for(int j=1;j<=x+1;++j){
        temp[i]=j;
        dfs1(i+1,max(j,x));
    }
}
inline bool issame(int a[],int b[]){
    for(int i=1;i<=m;++i)
        if(a[i]!=b[i]) return false;
    return true;
}
inline int pow(int x,int k){
    int re=1;if(k<0) return re;
    for(;k;k>>=1,x*=x) if(k&1) re*=x;
    return re;
}
void get1(int id){//求sta[id]此时转移到的状态a 
    int a[7];
    memcpy(a,sta[id],sizeof(sta[id]));
    for(int i=1;i<=m;++i) if(edge[i]){ 
        if(a[m+1]==0) a[m+1]=a[i];
        else{
            int x=a[i];
            for(int j=1;j<=m;++j) if(a[j]==x) a[j]=a[m+1];
        }
    }
    for(int i=1;i<=m;++i) a[i]=a[i+1];//把状态往前挪一个 
    int num[7],cnt=0;memset(num,0,sizeof(num));
    for(int i=1;i<=m;++i){//重新标号,使得满足最小表示法 
        if(!num[a[i]]) num[a[i]]=++cnt;
        a[i]=num[a[i]];
    }
    for(int i=1;i<=tot;++i) if(issame(a,sta[i])){
        trans.mat[id][i]++;return;
    }
}
void dfs2(int id,int i){//搜出所有sta[id]能转移到的状态,枚举下一个点与哪些点连边 
    if(i==m+1){get1(id);return;}
    dfs2(id,i+1);
    if(!f[sta[id][i]]){
        f[sta[id][i]]=1;edge[i]=1;dfs2(id,i+1);
        f[sta[id][i]]=0;edge[i]=0;
    }
}
void getinit(int id){
    int cnt[7];
    memset(cnt,0,sizeof(cnt));ini[id]=1;
    for(int i=1;i<=m;++i) cnt[sta[id][i]]++;
    for(int i=1;cnt[i]!=0;++i) ini[id]*=pow(cnt[i],cnt[i]-2);return;
    //n个点的完全图的生成树个数为n^(n-2)个 
}
int main(){
//  freopen("a.in","r",stdin);
    scanf("%d%lld",&m,&n);
    dfs1(1,0);//预处理出k个点的所有连接状态,用最小表示法表示 
    for(int i=1;i<=tot;++i) getinit(i);//预处理所有状态的初始种类数 
    for(int i=1;i<=tot;++i){
        memset(f,0,sizeof(f));
        memset(edge,0,sizeof(edge));
        bool flag=1;
        for(int j=2;j<=m;++j)
            if(sta[i][j]==1){flag=0;break;}
        if(flag){//1必须连这个了。 
            f[1]=1;edge[1]=1;dfs2(i,2);
        }
        else dfs2(i,1);
    }
    trans=trans^(n-m);
    for(int i=1;i<=tot;++i) ans=(ans+(ll)trans.mat[i][1]*ini[i])%mod;
    printf("%d\n",ans);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 有一个 $n$ 个点的棋盘,每个点上有一个数字 $a_i$,你需要从 $(1,1)$ 走到 $(n,n)$,每次只能往右或往下走,每个格子只能经过一次,路径上的数字和为 $S$。定义一个点 $(x,y)$ 的权值为 $a_x+a_y$,求所有满足条件的路径中,所有点的权值和的最小值。 输入格式 第一行一个整数 $n$。 接下来 $n$ 行,每行 $n$ 个整数,表示棋盘上每个点的数字。 输出格式 输出一个整数,表示所有满足条件的路径中,所有点的权值和的最小值。 数据范围 $1\leq n\leq 300$ 输入样例 3 1 2 3 4 5 6 7 8 9 输出样例 25 算法1 (树形dp) $O(n^3)$ 我们可以先将所有点的权值求出来,然后将其看作是一个有权值的图,问题就转化为了在这个图中求从 $(1,1)$ 到 $(n,n)$ 的所有路径中,所有点的权值和的最小值。 我们可以使用树形dp来解决这个问题,具体来说,我们可以将这个图看作是一棵树,每个点的父节点是它的前驱或者后继,然后我们从根节点开始,依次向下遍历,对于每个节点,我们可以考虑它的两个儿子,如果它的两个儿子都被遍历过了,那么我们就可以计算出从它的左儿子到它的右儿子的路径中,所有点的权值和的最小值,然后再将这个值加上当前节点的权值,就可以得到从根节点到当前节点的路径中,所有点的权值和的最小值。 时间复杂度 树形dp的时间复杂度是 $O(n^3)$。 C++ 代码 算法2 (动态规划) $O(n^3)$ 我们可以使用动态规划来解决这个问题,具体来说,我们可以定义 $f(i,j,s)$ 表示从 $(1,1)$ 到 $(i,j)$ 的所有路径中,所有点的权值和为 $s$ 的最小值,那么我们就可以得到如下的状态转移方程: $$ f(i,j,s)=\min\{f(i-1,j,s-a_{i,j}),f(i,j-1,s-a_{i,j})\} $$ 其中 $a_{i,j}$ 表示点 $(i,j)$ 的权值。 时间复杂度 动态规划的时间复杂度是 $O(n^3)$。 C++ 代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值