zoukankan      html  css  js  c++  java
  • 动态规划初步

    啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊我dp菜死了快来补一下。基本没讲解,来就放代码,偶尔写两句,也是在瞎扯。

    简介

    动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。

    线性DP

    LIS

    $O(nlong)$的方法用二分优化。

    #include<bits/stdc++.h>
    using namespace std;
    int n,a[100005],f[100005],cnt;
    int main()
    {
        ios::sync_with_stdio(false);
        cin>>n;
        for(int i=1;i<=n;i++)cin>>a[i];
        for(int i=1;i<=n;i++)
        {
            if(f[cnt]<a[i])f[++cnt]=a[i];
            else 
            {
                int mid=upper_bound(f+1,f+1+cnt,a[i])-f;
                f[mid]=a[i];
            }
        }
        cout<<cnt<<"
    ";
        return 0;
    }

    LCS

    困死我了。

    #include<bits/stdc++.h>
    using namespace std;
    int f[5005][5005],n;
    char a[5005],b[5005];
    int main()
    {
        ios::sync_with_stdio(false);
    //    cin>>n;
        scanf("%s",a);
        scanf("%s",b);
        int la=strlen(a),lb=strlen(b);
        for(int i=1;i<=la;i++)
            for(int j=1;j<=lb;j++)
            {
                if(a[i-1]==b[j-1])f[i][j]=max(f[i-1][j-1]+1,f[i][j]);
                f[i][j]=max(f[i][j],max(f[i-1][j],f[i][j-1]));
            }
        cout<<f[la][lb]<<"
    ";
        return 0;
    }

    LCIS

    我真的好困。。

    #include<bits/stdc++.h>
    using namespace std;
    int n,a[5005],b[5005],f[5005][5005];
    int main()
    {
        ios::sync_with_stdio(false);
        cin>>n;
        for(int i=1;i<=n;i++)cin>>a[i];
        for(int j=1;j<=n;j++)cin>>b[j];
        for(int i=1;i<=n;i++)
        {
            int val=0;
            if(b[0]<a[i])val=f[i-1][0];
            for(int j=1;j<=n;j++)
            {
                if(a[i]==b[j])f[i][j]=val+1;
                else f[i][j]=f[i-1][j];
                if(b[j]<a[i])val=max(val,f[i-1][j]);
            }
        }
        int maxn=0;
        for(int i=1;i<=n;i++)maxn=max(maxn,f[n][i]);
        cout<<maxn<<"
    ";
        return 0;
    }

    背包问题

    背包是一类经典dp。

    01背包

    有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

    关于初始化:

    如果要求了把背包放满,那么初始化f[0]=0,f[1~n]=-INF。

    如果没有要求放满,初始化为0。

    所有的背包问题基本都这样初始化。

    我们把j从m循环到w[i]:

    1.寻找放了w[i]后剩余空间的最大值。

    2.从大到小循环,保证当前状态的上一个状态不会出现已有当前物品的情况。

    #include<cstdio>
    #include<algorithm>
    using namespace std; 
    int n,m,w[205],c[205],f[205];
    int main()
    {
        scanf("%d%d",&m,&n);
        for(int i=1;i<=n;i++)
        scanf("%d%d",&w[i],&c[i]);
        for(int i=1;i<=n;i++)
        for(int j=m;j>=w[i];j--) 
        f[j]=max(f[j],f[j-w[i]]+c[i]);
        printf("%d
    ",f[m]);
        return 0;
    }

    完全背包

    有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

    和0~1背包的唯一区别就是有可能一个物品放多种。

    反正是同样的背包,你换一换第二维循环顺序不就行了吗?这样其实就是从绝不可能重复到可能重复,刚好满足完全背包的要求,理解清了0/1的循环顺序,这个也就没问题了。

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    int n,m,w[205],c[205],f[205];
    int main()
    {
        scanf("%d%d",&m,&n);
        for(int i=1;i<=n;i++)
        scanf("%d%d",&w[i],&c[i]);
        for(int i=1;i<=n;i++)
        for(int j=w[i];j<=m;j++)
        f[j]=max(f[j],f[j-w[i]]+c[i]);
        printf("max=%d
    ",f[m]);
        return 0;
    }

    多重背包

    有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

    这个题可以用二进制拆分思想来做,把每一个n[i]的系数化为1,2,4,...,2^(k-1),n[i]-2^k+1。

    假如有一个n[i]=13,那么13=(2^0)+(2^1)+(2^2)+13-(2^3)+1,k在此处为3。

    这样可以保证0~n[i]区间的每一个整数都可以用这些系数相加得到。

    所以该问题能转化为0/1背包。

    注意处理n-(2^k)+1。

    多重背包复杂度O(vΣNi=1logn[i])

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std; 
    int n,type,m,v,wt,num;
    int T;
    struct node
    {
        int w=0,c=0;
    };
    int f[1005];
    int main()
    {
        scanf("%d",&T);
        while(T--)
        {
            n=0;
            node a[1005];
            scanf("%d%d",&m,&type);
            while(type--)
            {
                scanf("%d%d%d",&v,&wt,&num);
                int p=1;
                while(num-p>0)
                {
                    num-=p;
                    a[++n].w=v*p;
                    a[n].c=wt*p;
                    p<<=1;
                }
                a[++n].w=num*v;
                a[n].c=wt*num;
                //注意要处理n[i]-(2^k)+1这个情况 
            }
            memset(f,0,sizeof(f));
            for(int i=1;i<=n;i++)
            for(int j=m;j>=a[i].w;j--)
            f[j]=max(f[j],f[j-a[i].w]+a[i].c);
            printf("%d
    ",f[m]);
        }
        return 0;
    }

     单调队列优化多重背包以后再写。

    混合背包

    就是把01,完全,多重这三种背包混合在一起。

    给一道题

    思路:

    for i=1..N

        if 第i件物品是01背包

            ZeroOnePack(c[i],w[i])

        else if 第i件物品是完全背包

            CompletePack(c[i],w[i])

        else if 第i件物品是多重背包

            MultiplePack(c[i],w[i],n[i])

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    int n,m,f[205],cnt;
    struct node
    {
        int w,c,flag;
    }a[205];
    int main()
    {
        scanf("%d%d",&m,&n);
        while(n--)
        {
            int ww,cc,p;
            scanf("%d%d%d",&ww,&cc,&p);
            if(p==0){a[++cnt].w=ww,a[cnt].c=cc;continue;}
            else
            {
                int mul=1;
                while(p-mul>0)
                {
                    p-=mul;
                    a[++cnt].c=mul*cc;
                    a[cnt].w=mul*ww;
                    a[cnt].flag=1;
                    mul<<=1;
                }
                a[++cnt].c=p*cc;
                a[cnt].w=p*ww;
                a[cnt].flag=1;
            }
        }
        for(int i=1;i<=cnt;i++)
        {
            if(a[i].flag)
                for(int j=m;j>=a[i].w;j--)
                f[j]=max(f[j],f[j-a[i].w]+a[i].c);
            else 
                for(int j=a[i].w;j<=m;j++)
                f[j]=max(f[j],f[j-a[i].w]+a[i].c);
        }
        printf("%d
    ",f[m]);
        return 0;
    }

    看到这里累了吗?我也好累,不过挺开心的。qwq

    二维费用背包

    就是说有两种背包容量,两种体积(咋好理解咋来ber),我这里给出01二维背包,正常人都会推广到二维费用完全、多重、混合。

    放个题,一起压榨kkk

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    int n,M,T,m[205],t[205],f[205][205];
    int main()
    {
        scanf("%d%d%d",&n,&M,&T);
        for(int i=1;i<=n;i++)
        scanf("%d%d",&m[i],&t[i]);
        for(int i=1;i<=n;i++)
        for(int j=M;j>=m[i];j--)
        for(int k=T;k>=t[i];k--)
        f[j][k]=max(f[j][k],f[j-m[i]][k-t[i]]+1);
        printf("%d
    ",f[M][T]);
        return 0;
    }

    分组背包

    有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

    其实就是01背包上面加了一维,假设划分为m组,每一组只能选一个,那么在01基础上套一维就行,不过要注意每一维的顺序,这很重要。如果顺序无法独立写正确,那么就代表没有理解分组背包的思想。

    最外层的i好理解。

    j也很好理解,把j放在cnt[i]的外面,其原因是每一个j只能由1~cnt[i]中的一个物品转移而来,最终会保留j在1~cnt[i]中的最优解,也就是说针对第i组,每一个j我们只选出一个k作为最优解,然后去循环下一组。

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    int n,m,cnt[205],T,f[205];
    struct node
    {
        int w,c;
    }a[15][35];
    int main()
    {
        scanf("%d%d%d",&m,&n,&T);
        while(n--)
        {
            int ww,cc,p;
            scanf("%d%d%d",&ww,&cc,&p);
            a[p][++cnt[p]].w=ww;
            a[p][cnt[p]].c=cc;
        }
        for(int i=1;i<=T;i++)
        for(int j=m;j>=0;j--)
        for(int k=1;k<=cnt[i];k++)
        if(j-a[i][k].w>=0)f[j]=max(f[j],f[j-a[i][k].w]+a[i][k].c);
        printf("%d
    ",f[m]);
        return 0;
    }

    背包问题的变形

    在背包问题中,很多时候不只是要求最大价值,还有很多有趣的变形。

     输出方案

    平时我们基本不会求方案数,但实际上方案在我们求解的过程中就已经体现了出来,以01背包举例。

    $f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}$,那么我们可以设置一个数组$g[i][v]$,若$f[i][v]=f[i-1][v]$,那么$g[i][v]=0$,否则$g[i][v]=1$,也就是表示第$i$项选或不选,最后输出。

    伪代码如下

    i=N
    
    v=V
    
    while(i>0)
    
        if(g[i][v]==0)
    
            print "未选第i项物品"
    
        else if(g[i][v]==1)
    
            print "选了第i项物品"
    
            v=v-c[i]

    输出字典序最小的最优方案

    这个我也不怎么理解,我也懒得在背包上面耗费过多时间。

    简单来说,逆序排序物品,再正常跑方案数即可。

    注意一个特殊情况,如果$f[i][v]=f[i-1][v]$和$f[i][v]=f[i-1][v-w[i]]+c[i]$是同时成立的,我们把后者的贡献计入答案而不是前者。

    方案总数

    在我们已经得到了诸多条件的时候,我们不仅可以求出最大贡献,还可以求出把背包装满或者装至某个容量的方案数,只需要把$max$改成$sum$。

    $f[i][v]=f[i-1][v]+f[i-1][v-w[i]]$

    最优方案总数

    不同于方案总数,我们加上了“最优”的限制条件,很明显我们要考虑价值,那么分类讨论。

    如果$f[i][j]=f[i-1][j]$,那么$g[i][j]=g[i-1][j]$

    如果在当前状态下$f[i][j]=f[i-1][j-w[i]]+c[i]$,那么$g[i][j]=g[i-1][j-w[i]]$。

    如果在当前状态下$f[i][j]=f[i-1][j]=f[i-1][j-w[i]]+c[i]$,那么$g[i][j]=g[i-1][j]+g[i-1][j-w[i]]$。

    求第k优解

    在最优解的基础上多了一个$K$的复杂度,只需要把原来的解排序(最优解到最不优解)然后统计,最后求出来就行了。

    初始化?可以不写。

    给一道题

    #include<bits/stdc++.h>
    using namespace std;
    int T,N,V,K,f[1005][35],c[1005],v[1005],tmp1[1005],tmp2[1005];
    int main()
    {
        scanf("%d",&T);
        while(T--)
        {
            memset(f,0,sizeof f);
            memset(tmp1,0,sizeof tmp1);
            memset(tmp2,0,sizeof tmp2);
            scanf("%d%d%d",&N,&V,&K);
            for(int i=1;i<=N;i++)scanf("%d",&c[i]);
            for(int i=1;i<=N;i++)scanf("%d",&v[i]);
            for(int i=1;i<=N;i++)
                for(int j=V;j>=v[i];j--)
                {
                    for(int k=1;k<=K;k++)
                    tmp1[k]=f[j][k],tmp2[k]=f[j-v[i]][k]+c[i];
                    tmp1[K+1]=tmp2[K+1]=-1;
                    int cnt=1,x=1,y=1;
                    while(cnt!=K+1&&(tmp1[x]!=-1||tmp2[x]!=-1))
                    {
                        if(tmp1[x]>tmp2[y])f[j][cnt]=tmp1[x],x++;
                        else f[j][cnt]=tmp2[y],y++;
                        if(f[j][cnt]!=f[j][cnt-1])cnt++;
                    }
                }
            printf("%d
    ",f[V][K]); 
        }
        return 0;
    }

    区间DP

    例1 [NOI1995]石子合并

    传送门

    首先断环成链,考虑设计状态,$f[l][r]$表示把第$l$到第$r$堆石子合并直到$l$,$r$相邻的分数,那么我们可以设计一个中间决策$k$,表示$l$~$r$可以由$l$~$k$和$k+1$~$r$转移过来,这就有点像$Floyd$算法的寻找中间点,有一点分治的意味在里面,但是我们合并$l$~$r$时,不仅仅有中间转移的分数贡献,而且还有$sum_{i=l}^{r}a_i$的分数贡献,因为每一次的合并算上的分数都是所有石子的分数贡献。

    这里求了一个$min$和$max$,于是我们可以设计两个$f$数组,求出不同的分数。

    注意边界问题和初始化,$max$和$min$要初始化为极大值或者极小值。

    #include<bits/stdc++.h>
    using namespace std;
    int n,a[5005],b[5005],f[5005][5005];
    int main()
    {
        ios::sync_with_stdio(false);
        cin>>n;
        for(int i=1;i<=n;i++)cin>>a[i];
        for(int j=1;j<=n;j++)cin>>b[j];
        for(int i=1;i<=n;i++)
        {
            int val=0;
            if(b[0]<a[i])val=f[i-1][0];
            for(int j=1;j<=n;j++)
            {
                if(a[i]==b[j])f[i][j]=val+1;
                else f[i][j]=f[i-1][j];
                if(b[j]<a[i])val=max(val,f[i-1][j]);
            }
        }
        int maxn=0;
        for(int i=1;i<=n;i++)maxn=max(maxn,f[n][i]);
        cout<<maxn<<"
    ";
        return 0;
    }

    例2 NOIP2003 数字游戏

    这道题首先仍然要断环成链,然后进行$O(n^4)$复杂度的$dp$,状态转移的过程仍然是在当前区间内寻找一个中间点(或者说断点),然后根据题目要求的$min/max$设计状态转移,这道题相比上一道题,看似更复杂,实际上不用考虑最后加一个前缀和,好理解很多,属于真正的区间$dp$入门。

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    using namespace std;
    int n,m,maxn[105][105][15],minn[105][105][15],ma,mi,sum[105],a[105];
    int mod(int a){return ((a%10)+10)%10;}
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),a[i+n]=a[i];
        for(int i=1;i<=2*n;i++)
        sum[i]=sum[i-1]+a[i];
        memset(minn,0x7f7f7f7f,sizeof(minn));
        for(int l=1;l<=2*n;l++)
        for(int r=1;r<=2*n;r++)
        maxn[l][r][1]=minn[l][r][1]=mod(sum[r]-sum[l-1]);
        for(int i=2;i<=m;i++)
        for(int l=1;l<=2*n;l++)
        for(int r=l+i-1;r<=2*n;r++)
        for(int k=l+i-2;k<r;k++)
        minn[l][r][i]=min(minn[l][r][i],minn[l][k][i-1]*mod(sum[r]-sum[k])),maxn[l][r][i]=max(maxn[l][r][i],maxn[l][k][i-1]*mod(sum[r]-sum[k]));
        mi=0x7f7f7f7f;
        for(int i=1;i<=n;i++)
        ma=max(ma,maxn[i][i+n-1][m]),mi=min(mi,minn[i][i+n-1][m]);
        printf("%d
    %d
    ",ma,mi);
        return 0;
    }

    树形DP

    全世界$DP$最菜的人居然在写树形$DP$的$Blog$,好耍了。

    例1 没有上司的舞会

    [传送门](https://www.luogu.org/problem/P1352)

    这道题对应了树形$DP$的一个经典模型,就是关于$0/1$之间的树形$DP$,大概的模型为:

    $f[u][1]+=max(f[v][1],f[v][0])$

    第一维表示的是以节点$u$为根节点的子树,第二维表示选/不选。

    #include<bits/stdc++.h>
    using namespace std;
    int n,f[12000][2],vis[6005],a[6005],h[12005],cnt;
    struct node{int v,nxt;}e[12005];
    void add(int u,int v)
    {
        e[++cnt].v=v;
        e[cnt].nxt=h[u];
        h[u]=cnt;    
    }
    void dfs(int u,int fa)
    {
        f[u][1]=a[u];
        for(int i=h[u];i;i=e[i].nxt)
        {
            int v=e[i].v;
            if(v==fa)continue;
            dfs(v,u);
            f[u][1]+=f[v][0];
            f[u][0]+=max(f[v][0],f[v][1]);
        }
    }
    inline void read(int&x)
    {
        x=0;char c=getchar();
        int f=1;
        while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
        while(c>='0'&&c<='9')x=x*10+c-'0',c=getchar();
        x*=f;
    }
    int main()
    {
        read(n);
        for(int i=1;i<=n;i++)read(a[i]);
        for(int i=1;i<=n;i++)
        {
            int u,v;
            read(u),read(v);
            if(!u&&!v)break;
            vis[v]=true;
            add(u,v),add(v,u);
        }
        int rt;
        for(int i=1;i<=n;i++)
        if(!vis[i]){rt=i;break;}
        dfs(rt,0);
        printf("%d
    ",max(f[rt][0],f[rt][1]));
        return 0;
    }

     例2 最大子树和

    [传送门](https://www.luogu.org/problem/P1122)

    很水的一道题,就是题意一点也不清晰。

    #include<bits/stdc++.h>
    using namespace std;
    int n,a[200005],h[320005],cnt,f[320005],maxn,vis[320005];
    struct node{int v,nxt;}e[320005];
    void add(int u,int v)
    {
        e[++cnt].v=v;
        e[cnt].nxt=h[u];
        h[u]=cnt;
    }
    void dfs(int u,int fa)
    { 
        f[u]=a[u];
        for(int i=h[u];i;i=e[i].nxt)
        {
            int v=e[i].v;
            if(v==fa)continue;
            dfs(v,u);
            f[u]+=max(f[v],0);
            
        }
        maxn=max(maxn,f[u]);
    }
    int main()
    {
    //    freopen("testdata.in","r",stdin);
        scanf("%d",&n);
        for(int i=1;i<=n;i++)scanf("%d",&a[i]);
        for(int i=1;i<n;i++)
        {
            int u,v;
            scanf("%d%d",&u,&v);
            add(u,v),add(v,u);
        }
        dfs(1,0);
        printf("%d
    ",maxn);
        return 0;
    }

     例3 选课

    传送门

    这道题是树上依赖背包的模板题。

    我们可以这样思考:节点$u$的子树大小为$siz[u]$,那么在这棵树中我们最多能选出$siz[u]$个(当然要包含这个节点本身)节点作为答案的贡献,所以,这个节点对答案的贡献可以是$0$~$siz[u]$,那么我们可以设计状态$f[u][j]$用来表示在以$u$为根的子树中选$j$个节点的贡献, 针对每一个这个点的子节点,我们要么不选,要么选,选了这个点对应的情况就应该是,在节点$u$的子树中总共选择$j$个节点($j$对应的遍历就应该是$siz[u]$~$0$),同时我们要选择子节点$v$,所以我们就还要在$v$节点的子树中选择一些节点,这个节点数是否可以直接看成$j-1$呢?不可以,因为剩下的$j-1$个节点在$u$和$v$的子树中分布不能肯定,所以就要单独枚举一维$k$,从$j-1$遍历到$0$,这一步应该是已经处理出来的,不要问我为什么,于是最终得到伪代码:

    $ exttt{for j siz[u]...0}$

      $ exttt{for k j-1...0}$

        $ exttt{f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+w[v])}$

    于是我们发现,这就是一个分组背包模型(虽然似乎好像应该可能也许大概有点变化)。

    还剩下一个问题就是,为什么两维循环都是倒序的呢?

    观察第一维度,我们倒序的时候,求出的$j$答案就是由大到小转移而来,如果我们按照顺序排序,前面已经更新过的$f$就会去更新后面的$f$,这就要出问题。

    第二维倒序,思想源自$01$背包,保证不重复。

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,w[305],f[305][305],siz[305],h[305],cnt;
    struct node{int v,nxt;}e[605];
    void add(int u,int v)
    {
        e[++cnt].v=v;
        e[cnt].nxt=h[u];
        h[u]=cnt;
    }
    void dfs(int u,int fa)
    {
        siz[u]=1;
        for(int i=h[u];i;i=e[i].nxt)
        {
            int v=e[i].v;
            if(v==fa)continue;
            dfs(v,u);
            siz[u]+=siz[v];
            for(int j=siz[u];j>=0;j--)
                for(int k=j-1;k>=0;k--)
                f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+w[v]);
        }
    }
    int main()
    {
        scanf("%d%d",&n,&m);
        for(int i=1;i<=n;i++)
        {
            int fa;
            scanf("%d%d",&fa,&w[i]);
            add(fa,i),add(i,fa);
        }
        dfs(0,-1);
        printf("%d
    ",f[0][m]);
        return 0;
    }

    状压DP

    仔细想想,我讲不来状压DP,因为我也是个初学的萌新。

    状压的核心就是,利用二进制位来枚举状态。

    直接上题吧。

    [SCOI2005]互不侵犯

    传送门

    题目描述:

    在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。(1<=N <=9, 0 <= K <= N * N)

    分析:

    这是一道状压的经典入门题。由于是矩阵,在状压的套路中要么枚举行要么枚举列,这里我们就枚举一波行数。

    我们考虑,每一行有(1<<n-1)种状态,有合法也有不合法,我们不妨预处理一行中能出现的所有合法状态,那么针对行的合法状态是什么呢?就是不能相邻两个都为1,“都为1”我们显而易见能想到&运算。

    如果有一个状态是i,如果i&(i<<1)的结果为1,那么就说明i这个状态是不合法的。为什么呢?举个例子:

    有一个状态i为0110,人眼判断是不合法的,这个状态左移就应该是1100,两个状态相&,答案为真,所以状态i 0110就不合法。

    所以我们就得到了预处理i是否合法的方法:i&(i<<1)。

    下一步需要预处理出所有的状态数和每一个状态有多少个1(也就是说有多少个国王),这里也是位运算的知识qwq,于是我们可以得到:

        for(int i=0;i<=(1<<n)-1;i++)//i表状态 
        {
            if(i&(i<<1))continue;
            s[++cnt]=i;
            num[cnt]=get(i);
            f[1][cnt][num[cnt]]=1;
        }

    这当中有一个f数组是干什么的呢?我们设f[i][j][k]表示:到达第i行的状态j时,总共放了k个国王的方案数。

    由于第一行不可以通过状态转移得到,所以我们预处理出来,如果这个状态合法,f[1][j][num[j]]的方案数就应该是1。

    状态该怎么转移呢?第i行的某个状态应该由第i-1行的某个状态转移而来:

    f[i][j][l]=f[i-1][k][l-num[j]]

    只需要判断j和k这两个状态同时存在是否合法即可,合法才能进行转移,否则跳过。

    不合法的判断:

    if((s[j]&s[k])||((s[j]<<1)&s[k])||((s[k]<<1)&s[j]))continue;

    Points:

    这道题是求一个总方案数,一个新的状态可以由多种不同方案转移而来,所以新的状态要加上所有能转移出它的所有状态的方案数,这里的j和k都表示的是第几种状态,所以范围是1~cnt,而l至少应该等于num[j],才能保证放的国王是足量的,也就是说在i,j状态时,总共放了l个国王,那么这肯定是l-num[j]的每一个状态转移而来的。

    感觉自己说了好多废话..无所谓,能理解就行。

    统计方案数就应该是最后一排每一个状态选了k个国王的总和,毕竟是统计总方案数。

    代码:

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define int long long
    using namespace std;
    int n,m,f[20][1<<10][105],s[1<<10],num[1<<10],cnt;//f[i][j][k] 第i行状态为第j种 这一行有k个国王 
    int get(int pos)
    {
        int ans=0;
        while(pos)
        {
            if(pos&1)
            ans++;
            pos>>=1;
        }
        return ans;
    }
    signed main()
    {
        scanf("%lld%lld",&n,&m);
        for(int i=0;i<=(1<<n)-1;i++)//i表状态 
        {
            if(i&(i<<1))continue;
            s[++cnt]=i;
            num[cnt]=get(i);
            f[1][cnt][num[cnt]]=1;
        }
        for(int i=2;i<=n;i++)
            for(int j=1;j<=cnt;j++)
                for(int k=1;k<=cnt;k++)
                {
                    if((s[j]&s[k])||((s[j]<<1)&s[k])||((s[k]<<1)&s[j]))continue;
                    for(int l=num[j];l<=m;l++)
                    f[i][j][l]+=f[i-1][k][l-num[j]];
                }
        int ans=0;
        for(int i=1;i<=cnt;i++)
        ans+=f[n][i][m];
        printf("%lld
    ",ans);
        return 0;
    }

     Matrix

    题面不公开,在此做个小总结。

    1.DP的核心思想是枚举,把我们要求但不能直接得到的决策进行枚举,适用于任何DP。

    2.矩阵状压有个特色就是,存行或者列,枚举另一维的所有状态。

    3.状压的核心思想是位运算,除此之外和普通DP差别不大,所以我们要熟练掌握各类位运算。

    4.状压的过程当中常常会遇到很多的要求,要求不相邻等等,我们要学会把这些要求转化成位运算的代码。

    5.初始化,状态与状态转移,这就是DP的过程 ,但我们只有找到了转移的方法,才知道每一个状态是怎么来的,自然也就知道了初始化。

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    int n,m,chu[15],f[15][1050][1050],g[15][15],w[15][1050],fg[1050];
    int main()
    {
        freopen("matrix.in","r",stdin);
        freopen("matrix.out","w",stdout);
        memset(f,0x7f7f7f7f,sizeof(f)); 
        scanf("%d%d",&n,&m);
        int all=(1<<m)-1;
        for(int i=1;i<=n;i++)
        {
            char s[15];
            scanf("%s",s+1);
            for(int j=1;j<=m;j++)
            chu[i]=(chu[i]<<1)|(s[j]-'0');//初始化每一行的状态 
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=m;j++)
                scanf("%d",&g[i][j]);    
            for(int j=0;j<=all;j++)
                for(int k=1;k<=m;k++)
                if((j>>(m-k))&1)w[i][j]+=g[i][k];    //针对选择区间进行初始化 第i行的第j个状态进行初始化 
        }
        for(int i=0;i<=all;i++)fg[i]=(i|(i<<1)|(i>>1))&all;//初始化选择区间的覆盖区间 
        for(int i=0;i<=all;i++)f[1][fg[i]|chu[1]][i]=w[1][i];//i枚举的是选择区间 
        for(int i=1;i<=n;i++)//枚举第几行 
            for(int j=0;j<=all;j++)//枚举第i行的状态j 
                for(int k=0;k<=all;k++)//枚举第j个状态的选择区间k 
                {
                    if((j&fg[k])!=fg[k])continue;    
                    for(int l=0;l<=all;l++)//枚举第i+1行的选择区间
                    {
                        if((l|j)!=all)continue;
                        f[i+1][chu[i+1]|k|fg[l]][l]=min(f[i+1][chu[i+1]|k|fg[l]][l],f[i][j][k]+w[i+1][l]);
                    } 
                }
        int ans=0x7f7f7f7f;
        for(int i=0;i<=all;i++)
        ans=min(ans,f[n][all][i]);
        printf("%d
    ",ans);
        return 0;
    }
  • 相关阅读:
    Redis安装部署
    传输方式Topic和Queue的对比
    Hudson配置及使用
    linux 绿色版 bes 6.6服务安装
    LINUX磁盘管理
    并发用户数与 TPS 之间的关系
    性能测试场景
    计算并发用户数的五种方法
    让selenium自动化脚本运行的更快的技巧
    Jmeter学习
  • 原文地址:https://www.cnblogs.com/valentino/p/11740673.html
Copyright © 2011-2022 走看看