集训笔记——各种dp(dp杂谈)

2020年3月7日更新

今天讲了各种dp

由于很多dp之前寒假集训都讲过所以这次ROS回顾一下大部分之前讲过的dp也算是给ROS复习一下了(不然ROS好久不写也快忘了)

鉴于以前ROS由于很多东西没有听懂所以进化成了莫得感情的放ppt机器很多东西没有消化透所以只是简单的把ppt内的内容粘贴到这里。

而这一次ROS则会把我没有搞懂的东西以及知识点粘贴到这里,而会的东西则主要以自己的方式讲解。

开始啦!

(注意认真听课!)

开课讲了状压dp

课件内容是这样的:

• 我们目前碰到的题的状态都比较好表示。
• 但有时我们也会记录具体的状态。
• 此时为了节省时间和空间,我们就可以使用状态压缩来对状态压缩。
• 因为我们是记录具体的状态,所以数据范围一般不会很大,所以当你看到数据范围小但是暴力又过不去(或者写起来可能十分困难)的时候就要想到状压dp了。
 
所以根据本人的理解,状压dp是这么个东西(理解不深刻还请谅解):
表示价值或代价时,其所开的数组就与一般的dp相同,但是状压dp比一般dp还多一个状态压缩的维度,如果有两个状态则用二进制表示并转为十进制,n个状态就是n进制)
然后解题的时候就可以用一个十进制的数表示一个固定的价值了
大概就是这么个东西
 
好的现在开始讲题:
状压dp:
1.洛谷P1896 [SCOI2005]互不侵犯
原题:
• 在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。
• 1 <=N <=9, 0 <= K <= N * N
课件讲解:
• 不难发现,我们上一行如何放国王会影响下一行如何放国王,但是上两行及以上放的国王是不会影响到我们这一行放的国王的。
• 所以我们只需要记录上一行的国王放到了哪里。
• 我们令没有国王的格子为0,有国王的格子为1,那样我们就把这个状态变成了二进制了。
• 令f[i][j][k]表示前i行放j个国王且第i行排成k情况的时候的情况数有多少。
• 为了方便运算与表示,我们定义g[i]表示一行国王排成i情况的时候有几个国王。
• 显然,我们f[i][j][k]+=f[i-1][j-g[l]][l]但是前提l和k不能互相侵犯,并且自己行内的国王不能互相侵犯。
• 复杂度O(n^3*2^n*2^n)?
这一分析就是典型的状压dp类型题目!!
小trick:
• g怎么算?除了暴力?
• g[i]=g[i>>1]+(i&1);
• 如何判断状态i,j之间是否合法?
• 首先判断i和j本身是否合法,通过将本身左移,再和原状态&一下,如果不为0就一定撞上了。
• 再考虑i和j之间是否合法,同样的思路,将j左/右移/不动和i&(当然反过来也可以),如果不为0就一定撞上了(这分别对应了国王可以攻击左,中,右三个格子)
 
  • 背包dp:
声明:
• 这是一项大类
• 里面不涉及多重背包,混合背包,泛化物品等知识点(不常用(真不常用,有些算法我都没细细写过),但是都不难,建议课后你们去看看比如背包九讲之类的。

  Ⅰ.0/1背包问题:

2.洛谷P1048采药
原题:
• 有N件不可切割的物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
ROS自我讲解:
0/1背包问题在初学时难理解的话可以开二维数组f[n][v],但是我们稍微学一学就会知道其实开一个一维数组就足够了。f[v]即可以进行dp的过程。
转移方程:
f[j]=max(f[j],f[i-c[i]]+w[i])
j是当前访问容量,i是物品时第i件。
所以便很容易了。
课件讲解:
• 这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
• 不难想到f[i][v]表示前i件物品放入一个容量为v的背包可以获得的最大价值,这样答案即为f[N][M]。
• 初始化所有f为0,其状态转移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。(前者就是不放,后者就是放)
• 当然前提是能放得下物体,不然的话就要f[i][v]=f[i-1][v].
• 以上方法的时间和空间复杂度均为O(N*V)。
 
由0/1背包问题引出:
• 时间复杂度已经没法优化了,但是空间复杂度还可以进一步优化,这里就插播一种优化的方式:旋转数组
• 思考我要求fib第n项我需要开多大的数组?
• n+1?我可以告诉你只需要3个就行了。
• f[2]=f[1]+f[0];f[0]=f[1];f[1]=f[2].
• 上述循环n-2次,答案就是f[2]。
• 不难看出优化原理:我们每次需要的其实就是f[n],f[n-1],f[n-2],至于其他的项已经没有用了,所以完全可以扔掉。
 
回顾0/1:
• 现在我们再回顾背包问题,我们只需要f[i]和f[i-1],所以处理方法就如同前面所说就行,第一维完全可以滚掉,这样我们的空间复杂度就有保证了。
• 但其实我们压根不需要第一维。
• 我们设f[v]表示容量为v的背包可以获得的最大价值,递推式子没有变还是f[v]=max{f[v],f[v-c[i]]+w[i]};
• 但是有一个问题,可能这个f[v-c[i]]是已经放入了一遍物品,于是就有可能造成我们一件物品多拿的情况!
• 考虑到v-c[i]<v,我们在枚举第二维的时候直接倒着枚举就好了。
 
  Ⅱ.完全背包问题:
3.洛谷P1616疯狂的采药
先把课件看完:
• 与0/1背包不同的是每个物品都可以无限取。
• 于是f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]},后者的-1去掉了是因为我们虽然取了一次i物品但是我们仍然可以继续去取它。
• 与0/1背包不同的是每个物品都可以无限取。
• 于是f[i][v]=max{f[i-1][v],f[i][v-c[i]]+w[i]},后者的-1去掉了是因为我们虽然取了一次i物品但是我们仍然可以继续去取它。
• 当然,我们仍然可以优化掉前面的一维状态,还记得0/1为什么倒着循环吗?
• 我们正着循环就是完全背包的解法!
ROS:
所以说我们已经知道了类似的dp可以使用一维数组模拟dp,而在普通的采药问题中此一维数组要反着来枚举就是为了防止同一个药被采摘两次。
而在这里由于我们要“疯狂地”采药呀所以便要正着模拟,懂?
就是因为我们只有正着模拟才可能使得容量越大我采摘越多的草药!
AC代码:
 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cmath> 
 4 using namespace std;
 5 int t,m,a[10005],val[10005],dp[100050];
 6 int main()
 7 {
 8     scanf("%d%d",&t,&m);
 9     for(int i = 1;i <= m; i++){
10         scanf("%d%d",&a[i],&val[i]);
11     }
12     for(int i = 1;i <= m;i++){
13         for(int j = a[i];j <= t;j++)
14             dp[j] = max(dp[j],dp[j-a[i]]+val[i]);
15     }
16     printf("%d",dp[t]);
17     return 0;
18 }            

  Ⅲ.二位或多维费用:

4.洛谷P1885榨取kkksc03

传送门

课件概念及讲解:

• 二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
• 解法就是多加一维就好了,如果是0/1就按0/1循环,如果是完全就完全,道理都一样。
• 多维亦是如此。
 
  Ⅳ.分组背包
5.洛谷P1757通天之分组背包
课件:
• 实际上和普通的0/1背包一样,我们多加一维状态就好了。设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}。完后写三层循环即可。
ROS:
课件说得对!
再补充一点的话那就是其实用一维数组dp也是可以完成这个过程的!
但是ROS当时在做这道题的时候用了结构体,加上空间不是动态的所以也算耗了不少的空间...不过幸好数据范围比较小所以也AC了。
AC代码
#include<iostream>
#include<cstdio>
#include<queue>
#define M 1050
using namespace std;
int m,n;
int a,b,c,tot[105],zs;
int dp[1050];
struct th{
    int val;
    int w;
};
struct thing{
    th tt[1010];
};
thing t[110];
int main(){
    scanf("%d%d",&m,&n);
    for(int i=1;i<=n;i++){
        scanf("%d%d%d",&a,&b,&c);
        if(tot[c]==0) zs++;
        tot[c]++;
        t[c].tt[tot[c]].w=a;
        t[c].tt[tot[c]].val=b;
    }
    for(int i=1;i<=100;i++){            //枚举组数 
        if(!tot[i]) continue;            //如果这一组没有物品则跳过 
        for(int j=m;j>=0;j--){            //枚举每容量
            for(int k=1;k<=tot[i];k++){    //枚举每一组中的物品 
                if(t[i].tt[k].w<=j){
                    dp[j]=max(dp[j-t[i].tt[k].w]+t[i].tt[k].val,dp[j]);
                }
                else{
                    //dp[j]=max(dp[j-1],dp[j]);
                }
            }
        }
    }
    printf("%d",dp[m]);
    return 0;
}
    
    
    

  Ⅴ.背包变形

• 要求背包必须装满
• 仔细思考就能发现我们是怎么让背包装不满的呢?
• 思考只装一个第一个物体的时候f[v]=max{f[v],f[v-w[i]]+c[i]},我们是从f[v-w[i]]转移过来的,而v-w[i]不一定为0,也就是说v-w[i]为多少,最后这个背包就会剩下多大的空间。
• 所以说白了要求背包必须装满就是要求我们背包必须要从f[0]开始转移,于是我们有以下解决方法:
• 1.要求背包必须装满且求最大值
• f[0]=0, 其余为负无穷。
• 2, 要求背包必须装满且求最小值
• f[0]=0, 其余为正无穷。
 
• 输出具体方案
• 以0/1背包举例子。记录下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。

• 比如g[i][v]表示f[i][v]这层状态是否取了第i件物品,如果取了就是1,否则就是0。
• 然后我们从f[N][V]倒推,如果g[N][V]==1那就说明它取了第N件物品,那么它之前的状态就是f[N-1][V-w[N]];否则就没取,然后从f[N-1][V]转移来的。一直循环下去就能输出方案了。
• 但当然f不必真的设两维,g设两维就好了。
 
6.洛谷P1759输出字典序最小的具体方案
• 这要求我们转移的时候细致些,比如说取i和不取i得到的价值都是一样的的话我们就不能要。
• 但输出方案的时候请注意,我们输出的是反过来的,因为我们是从后往前推的。
• 比如说你取法为1 3 4,你输出就会得到4 3 1,所以需要事先存起来然后倒着输出。

求将背包装满方案数:
• 洛谷P1164,P1474
• 将f[i][j]定义改一下变成方案数即可,把max改成sum就好,因为这个状态可以从这两个状态转移来,所以方案数就是不取i的方案数+取i的方案数,即f[i][v]=sum{f[i-1][v],f[i-1][v-c[i]]+w[i]},初始条件f[i][0]=1。
• 当然本质上还是背包所以我们前一维可以去掉。
 
  求最优方案的总数:
• 为了省事直接用一维数组来讲,如果理解不能的话你就改用二维理解。
• f定义不变,我们再定义g表示方案总数
• 如果f[v]从f[v-w[i]]+c[i]更新过来了,那么g[v]=g[v-w[i]]
• 但是注意,如果取和不取一样的话,那么g[v]+=g[v-w[i]]
• 原理其实和上面一样。
 
原文地址:https://www.cnblogs.com/robertspot/p/12438709.html