DP例题较多,可以根据自己需求食用~

update:下翻有状压DP入门讲解,也只有讲解了(逃~

DP的实质,就是状态的枚举。

一般用DP解决的问题,都是求计数最优问题,所以这类问题,我们也可以用搜索来解决。

但是,之所以出现DP,就是因为在有些情况下,搜索不能以较高的效率求解,题目也不需要记录过程,而DP是直接记录答案,不记录过程,效率较高,所以深受广大OIer的唾弃喜爱。

所以写DP的难点就在如何表示每一个状态并利用它快速推出我们想得到的答案。

让我们通过一些毒瘤经典例题来练习DP吧awa

引入

1.走楼梯

题目传送门

明显这道题就是一个DP,还是很入门那种

但既然是引入,还是有必要讲讲的。

既然这位老哥一次可以跨1或2步,那么每一阶的方案数就是前两阶的方案数之和,即 dp[i]=dp[i-1]+dp[i-2]

当然,没有初始值递推是跑不起来的,这道题目的初始值就是 dp[0]=1,dp[1]=1站着不动1种,走一步1种。

然后从2~n推一遍,输出 dp[n]就可以了。

code

2.数塔问题

题目传送门

作为引入的第二道题目依然水到爆

很明显的,这道题目需要一点贪心的思想,每次选择左上和上方较大的一个,由较大那个数的dp值加上这个点本身输入的那个
值,最后遍历最后一行的dp值取max即可。

code

例题

1.传球问题

题目传送门

这道题初一看好像真的想不出该怎么做,但是在同机房大佬的提醒下 应该用DP!

我们用 dp[m][n]表示第m次传递后第n个小朋友的传娃娃方法数,很明显,我们需要求的即是 dp[m][1]的值。找到边界值:dp[0][1]=1;然后可以发现,一个状态转移方程是无法解决这个比较复杂的dp的,需要添加if语句达到效果。

于是我开始讨论有哪些可能。因为这道题是直接用的dp,并没有构建环,所以这是需要特殊考虑的。然后,我们发现,第i次传递后的点k的方案数,只能由第i-1次传递后的点k的左右两人的方案数之和来得到!

但是如上所述,当这个点是1或n时需要特判,所以情况分为三种:

①这个点是1时:dp[i][j]=dp[i-1][j+1]+dp[i-1][n];

②这个点是n时:dp[i][j]=dp[i-1][j-1]+dp[i-1][1];

③这个点是普通点(非1非n时):dp[i][j]=dp[i-1][j-1]+dp[i-1][j+1];

然后双重循环,外层1…->m,内层1…->n,完事输出 dp[m][1]即可。

code

2.最小乘车费用

题目传送门

dp题目,本质是完全背包。

完全背包不懂的可以去百度一下

状态转移方程式 dp[j]=min(dp[j],dp[j-i]+a[i]);

其实就是一个选不选选多少的问题,只不过循环的顺序需要注意一下。

另外,除了 dp[0]以外,其他的dp值都要赋为极大值(没选之前都是无效的所以要极大值)

code

3.最大子段和

题目传送门

这道题目暴力是很容易做出来的...但明显效率太低,重点讲一下从暴力到dp的方法。

1)暴力枚举左右端点,循环求值取max。

这种方法真的暴力。。时间复杂度达到了$O(n^3)$,在这道题目$n\leq200000$的数据范围下会TLE得很惨%%%

2)前缀和枚举优化

比方法1少了循环求值的过程,p[i]表示从1-n的值的和,求区间值直接相减就可以了。

时间复杂度$O(n^2)$,不出意外的依然会T

3)贪心/DP

这种方法是这道题目的正解,时间复杂度只有$O(n)$。

大致思路:设置一个cnt和一个maxx,maxx的初值为第一个输入的数,然后后面每次输入贪心,判断选这个数加入是否要更优,然后缩进左端点,更新cnt,更新ans(取max)

code

4.*最长公共子序列

跟dp没啥关系但是值得了解。

前置知识:求最长上升子序列

p.s. 子序列不是子段,子序列只要求在原串中按顺序出现,不需要连续出现

题目传送门

思路:

知道怎么求最长上升子序列就很简单。因为两个串都是1-n的一个排列,所以元素是相同的。

我们把第一个串的n个数分别对应1-n,让第一个串变成1-n的一个顺序排列(第i个数变成i)

然后按照这个规律,把第2个串也变一下(比如1串中3变成了5,那么2串中的3也要变成5)

然后在2串中找最长上升子序列就可以了,要用二分不然会炸。

原理很好理解,1串经过变化已经变成一个上升串了,又因为两个串中的元素都是相同的,所以找出2串中的最长上升子序列就可以了。

code

5.最大正方形

题目传送门

这道题是在一个01矩阵中找出最大的由1组成的最大的正方形的边长。

输入的原图我们用a数组存起来,当然是二维的。

然后我们用fi表示以点(i,j)为右下角的正方形的最大边长

拿样例来说,f2的值就是1,f3的值就是2(可以画一个2×2的正方形)

然后每次更新的时候判断,首先这个点的值得是1,因为表示的是右下角,所以就看左上,左和上的fi值,取min+1作为自己的f值。

为什么要这样做呢?很明显,如果自己的左上,左和上有任意一个是0,那么这个点的f值一定是1(取min+1)而如果它们都是1,那么这个点的f值就是2(能放出边长为2的正方形)

当然,如果这个点的左上,左和上都能作为右下角构建2×2的正方形,那么以这个点为右下角就能构建3×3的正方形(可以画图理解一下)

code

6.最大正方形II

题目传送门

和上一道题目类似,但是拓展时判断的标准从它的值是否是1变成了它是否和左上相同并且和左、上不同(因为要岔开)

代码中要修改的也只有这一处。。

code

*状态压缩DP

动态规划的状态有时候比较恶心,不容易表示出来,需要用一些编码技术,把状态压缩的用简单的方式表示出来。

典型方式:当需要表示一个集合有哪些元素时,往往利用2进制用一个整数表示。比如01背包问题,选表示成1,不选表示成0。

就拿'110101'来说,从低位来看,表示:1选,2不选,3选,4不选,5选,6选。n个物品选取方案的表示信息一定在$[0,2^n-
1]$之间。

动态规划本来就很抽象,状态的设定和状态的转移都不好把握,而状态压缩的动态规划解决的就是那种状态很多,不容易用一般的方法表示的动态规划问题,这个就更加的难于把握了。难点在于以下几个方面:状态怎么压缩?压缩后怎么表示?怎么转移?是否具有最优子结构?是否满足后效性?涉及到一些位运算的操作,虽然比较抽象,但本质还是动态规划。

先介绍一下常用的位运算:

  • 按位与:&,把操作的两个数的二进制按位与,两个数的第i位都是1,得到的数的第i位才是1.
  • 按位或:|,把操作的两个数的二进制按位或,任意一个数的第i位是1,得到的数的第i位才是1.
  • 按位异或:^(虽然它平时表示计算符号但是这里它是一个位运算符号):把操作的两个数的二进制按位异或,两个数的第i位
    不同,得到的数的第i位才是1.
  • 取反:~,只有一个操作数,把这个数的二进制的每一位都取反(0变1,1变0)
  • 左移:<<,把操作数的二进制数左移N位,低位用0补上
  • 右移:>>,把操作数的二进制数右移N位,被移出的舍去

有什么用呢?

它快啊!它的计算速度的确比普通的四则运算要快一些,并且也有一些奇怪神奇的作用。

比如树状数组中会用到的,利用了负数补码的性质,找出x最低位的1的位置,方便树状数组的使用:

int lowbit(int x)
{
    return x&(-x);
}

但是需要注意的,位运算的计算优先度较低,请配合括号食用awa

例题后面刷了再更新吧...


update 2020.10.21

两道期望dp入门的

P4316 绿豆蛙的归宿

点边有向无环图,从1开始等概率走向一个连向的点,求走到n的期望路径总长度。

设为号点走到号店的期望路径总长度,。答案就是。

那么假设有条从连向的边,那么

从n开始反向建图,在拓扑排序的过程中逆推。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int head[N],to[N],nxt[N],val[N],cnt;
void add(int u,int v,int w){
    to[++cnt]=v,val[cnt]=w,nxt[cnt]=head[u],head[u]=cnt;
}
int d[N],in[N];
double f[N];
int n,m;
void topo(){
    queue<int> q;
    q.push(n);
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=head[u];i;i=nxt[i]){
            int v=to[i],w=val[i];
            f[v]+=(f[u]+1.0*w)/d[v];
            if(!(--in[v]))q.push(v);
        }
    }
    return;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(v,u,w);
        in[u]++,d[u]++;
    }
    topo();
    printf("%.2lf\n",f[1]);
    return 0;
}

CF280C Game on tree

个节点的树,每次随机选择一个点将其及其子树染黑,问期望多少次可以将所有节点染黑。

考虑每个点被染黑的所有情况。

深度为的点被染黑,当且仅当它的0(本身)到d级父亲被染黑,所以它被染黑的期望次数是$\frac{1}{d}$。

每个节点相对独立,把所有点被染黑的期望次数求和即可。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int head[N],to[N],nxt[N],cnt;
void add(int u,int v){
    to[++cnt]=v,nxt[cnt]=head[u],head[u]=cnt;
}
int dep[N];
void dfs(int u,int f){
    dep[u]=dep[f]+1;
    for(int i=head[u];i;i=nxt[i]){
        if(to[i]!=f)dfs(to[i],u);
    }
}
int main(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<n;i++){
        int u,v;scanf("%d%d",&u,&v);
        add(u,v),add(v,u);
    }
    dfs(1,0);
    double ans=0;
    for(int i=1;i<=n;i++){
        ans+=1.0/dep[i];
    }
    printf("%.10lf\n",ans);
    return 0;
}
最后修改:2021 年 03 月 13 日 09 : 39 PM