动态规划

引例:

斐波那契数列:
边界条件:f0=0;

f1=1;

能够直接被求出值的状态

不需要计算其他斐波那契数列的值直接可以得到结果;

转移方程:fn=fn-1+fn-2如何用已有状态求出未知状态

前几项:0,1,1,2,3,5,8,13……

状态:f1,f2,f3……fn;(要求的未知的量)

DAG<=>无后效性??(暂时不用管什么东西)

通项公式:

实现DP

法1:记忆化搜索(一般来说用不上qwq)

会多开一个记录是否算过的数组,故空间会比下面两种大一点

法2:顺着推:用自己去推别人

法3:倒着推:用别人更新自己

#include<iostream>
#include<cstdio>
using namspace std; int n,f[];
/*斐波那契数列:倒着推
int main(){
cin>>n;
f[0]=0;
f[1]=1;
for(int a=2;a<n;a++)
{
f[a]=f[a-1]+f[a-2];
}
cout<<f[n]<<endl;
}*/
/*斐波那契数列:顺着推
int main(){
cin>>n;
f[0]=0;
f[1]=1;
for(int a=0;a<n;a++){
f[a+1]+=f[a];
f[a+2]+=f[a];
}
cout<<f[n]<<endl;
} */
/*搜索 O(f(n)) 与斐波那契数列第n项大小成正比 约为1.6^n
int dfs(int n){
if(n==0) return 0;
if(n==1) return 1;
return dfs(n-1)+dfs(n-2);
} int main(){
cin>>n;
cout<<dfs(n)<<endl; return 0
} */
/*记忆化搜索 O(n)
在计算过程中,保证f0~fn每个数只被算一次 bool suan_le_mei[n]; int dfs(int n){
if(n==0) return 0;
if(n==1) return 1;
if(suan_le_mei[n]) return f[n];//判断是否算过 suan_le_mei[n]=true;
f[n]=dfs(n-1)+dfs(n-2); return f[n];
} int main(){
cin>>n;
cout<<dfs(n)<<endl; return 0
}*/

常见DP种类:

数位DP-50%

树形DP-50%

状压DP-50%

区间DP-50%

(有套路^)

其他DP-80%

(无规律^)

考不到的DP:插头DP,博弈论DP,呸概率比较小

常见优化:

单调性优化

矩阵乘法优化

其他优化

放弃学习优化

看的一懵一懵的

数位DP

什么叫做数位DP?

按照数字的位数划分转移阶段

转移方式:枚举下一位数字填什么

限制条件:数位的上下界要求

读入两个正整数l,r,问从l~r有多少个数?

显然ans=r-l+1;

但是,我们的钟神拒绝平常,他要与众不同,他要用数位DP做qwq

第一步:转化:[0,r]数的个数-[0,l-1]的数的个数=>转化成解决[0,x]有多少数=>有多少个v使得0<=v<=x;

x=>xn xn-1 xn-2……x0

v=>vn vn-1vn-2……v0(至多n位)给每位填上0~9的数,看有多少种方案满足v<=x

从最高位=>最低位

举个栗子:填vn-3时,前面的都填好了

那么v的前面的三位必须保证小于等于x的前三位,那么为了使v<=x,则需要:

分两种情况:1.x的前三位大于v的前三位=>vn-3可以填任何数

2.x的前三位等于v的前三位=>0~xn-3

f[i][j(0/1)] 代表这种情况的方案数

i:已经填好了第i位,j=>0/1

j=0=>xnxn-1……xi>vnvn-1……vi

j=1=>xnxn-1……xi=vnvn-1……vi

转移:枚举第i-1位填什么

填第i位,从i+1位转移过来.

边界:第n+1位(相等且没有数)f[n+1][1]=1;(都填0)

#include<iostream>

using namespace std;

int l,r,z[];
int f[][]; int solve(int x){
int n=;
while(x){//求出x的每一位(最后有n位)(从0下标开始)
z[n]=x%;
x/=;
n++;
}
n--;
memset(f,,sizeof(f));//需要做两次DP,故要把数组清空 f[n+][]=;//边界条件:第n+1位显然都填0,所以相等的方案数为1
//不相等的方案数为0 for(int a=n;a>=;a--)//枚举要填的第a位
{
for(int b=;b<=;b++)//考虑相等还是小于分两种讨论qwq
if(b==){//x>v
for(int c=;c<=;c++)
f[a][]+=f[a+][b];
}
else {
for(int c=;c<=z[a];c++){
if(c==z[a])f[a][]+=f[a+][b]//c==z[a],代表填上这个数后v=x
else f[a][]+=f[a+][b];//c!=z[a],代表填上这个数后v<x
}
}
}
return f[][]+f[][];
} int main(){ cin>>l>>r; cout<<solve(r)-solve(l-)<<endl; return ;
}

problem2:

求[l,r]中的数的数位之和

#include<iostream>
#include<cstring> using namespace std; int l,r,z[];
int f[][];
int g[][];//ij表达与f相同 int solve(int x){
int n=;
while(x){//求出x的每一位(最后有n位)(从0下标开始)
z[n]=x%;
x/=;
n++;
}
n--;
memset(f,,sizeof(f));//需要做两次DP,故要把数组清空
memset(g,,sizeof(g)); f[n+][]=;//边界条件:第n+1位显然都填0,所以相等的方案数为1,不相等的方案数为0
g[n+][]=; for(int a=n;a>=;a--)//枚举要填的第a位
{
for(int b=;b<=;b++)//考虑相等还是小于分两种讨论qwq
if(b==){//x>v
for(int c=;c<=;c++){
f[a][]+=f[a+][b];
g[a][]+=g[a+][b]+f[a+][b]*c;//先加上前a+1位的和,然后对于第a位,可以填0~9任一,那么每填一个数c,每一种方案的和都+c,故要用方案数*c
}
}
else {
for(int c=;c<=z[a];c++){
if(c==z[a]){
f[a][]+=f[a+][b];
g[a][]+=g[a+][b]+f[a+][b]*c;
}//c==z[a],代表填上这个数后v=x
else {
f[a][]+=f[a+][b];//c!=z[a],代表填上这个数后v<x
g[a][]+=g[a+][b]+f[a+][b]*c;
}
}
}
}
//return f[0][0]+f[0][1];
return g[][]+g[][];
} int main(){ cin>>l>>r; cout<<solve(r)-solve(l-)<<endl; return ;
}

problem3:

求在[l,r]中的满足相邻两个数字之差至少为2的数有多少个

多一维条件,多一个状态。

f[i][j][k]已经填好了第i位;j:0/1表示</=;k:第i位填了k

保证第i位和第i+1位的数字大小差至少2;

windy数qwq

树形DP:

什么是树形DP:

按照树从根往下或者叶子网上划分阶段

转移方式:集合叶子或者父亲的信息

限制条件:不详

给你一个n个点的数,问有多少个点???闲圈啊qwq

f[i]以i为根的子树中共有多少个点

边界条件:叶节点的f为1;f[叶子]=1;

f[p]=f[p1]+f[p2]+1;

转移方程 f[i]=f[son1]+f[son2]+……+f[sonn]+1

eg:

给我一个n个点的数,求这棵树的直径是多少?

直径:在一棵树上找到两个点,使得这两点间的距离最远。

树路径长度,从根结点到某结点的边数。

考虑一棵子树内的直径qwq

路径长这样:

可以把直径看成从某一点向下走了两条路然后合起来

找两种向下走最长的两条路径

即从一点向下走最长可以走多少和次长可以走多少。

状态定义:f[i][0]i向下最长那条路长度是多少

f[i][1]i乡下的次长的那条路的长度

转移方程:(n个儿子)

f[p][0]=max(f[son1][0],f[son2][0]……,f[sonn][0])+1;

假设最大的是son2,那么显然我们不能再走son2了

f[p][1]=max(f[son1][0],f[son3][0],……f[sonn][0])+1;

ans=max(f[i][0]+f[i][1]);(枚举所有的i找最大的一个)

区间DP:

满足:

合并相邻的

把所有的合并成一个东西

的为区间DP;

eg:合并石子

n堆石头,a1,a2……an,可以合并相邻的两堆石子,合并的代价:合并的这两堆的石头数之和。(合并n-1次qwq)现在把这n堆石头合并为1堆,使代价最小。

状态:f[l][r]代表把第l堆石子到第r堆石子合并为一堆石子的最小代价

要求的即为f[1][n]

边界条件:l=r时答案为0;即f[i][i]=0;

转移方程:f[l][r]=min(f[l][p]+f[p+1][r]+sum[l][r])

合并过程并没有改变石头的顺序,最后一定为把两堆合并为一堆,那么一定可以找到一条分界线p,左边为al~ap右边为ap+1,~ar

左边合并的最小代价即为f[l][p],右侧为f[p+1][r];

枚举一个点p把左右分别合并成一堆,然后再合并成一堆

复杂度:O(n3)(n的极限约为100~200左右)

石子合并:洛谷p1880

处理第1堆和第n堆相邻的情况

很自然的想法:

最后的答案可能不是f[1][n]

an和a1相邻,a1,a2不相邻的情况:f[2][n+1]

再加一个a2:

继续向后面加:(在原来序列基础上补上一个相同的序列)

取min

原理:

对于一个环形排序的n堆物品,要把他们合并为一堆,需要合并n-1次,那么有一条边始终没有用到,我们可以看是断开的,也就是说,我们可以不看它,把长度延长为原来的两倍即为枚举要断开的边是哪一条


下午

状态压缩DP

什么叫状压DP:

按照选取集合的状态划分转移阶段

转移方式:枚举下一个要选取的物品

限制条件:不详

N个点:(x1,y1)(x2,y2)……(xN,yN

从一号点出发,保证每个点都至少走一次,使得走过的路径的长度之和最短

TSP问题(旅行商问题)非常经典的NP-hard问题(想解决掉这种问题,至少为2n,n在指数上)qwq

一个点有没有必要走两次???没有=>最优情况下每个点只需去一次

应该怎么走???直接拉一条线段过去<=>两点间线段最短

已走:1 2 5

未走:3 4 6

现在的位置在5号点,那么接下来的状态:

如何把集合表示到代码里面去:(如何用一个数代表一个集合)构造一个n位的二进制数,

<=>{1,4,5}

转成10进制=>25

每个元素只可能 在/不在 集合中两种情况,第i位在集合中为1,不在集合中为0

设计这样一个DP方法:

f[s][i]

s:n位的二进制数,已经走到过的点

:{1,2,4,6}说明已经走过1,2,4,6点,对应二进制=>101011

i:现在停留在i点

初始化:f[1][1]=0;

转移:

从i点到j点,j属于1~n;

并且要判断j∉s

f[s∪{j}][j]<=f[s][i]+dis[i][j];

p1171

#include<cstdio>
#include<iostream> using namespace std; const int manx=; int n;
double f[<<maxn][maxn];//f[2的n次方][n]; double dis(int i,int j){
return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
} int main(){ cin>>n;
for(int a=;a<n;a++)//二进制最低位是第0位
cin>>x[a]>>y[a];
for(int a=;a<(<<n);a++)
for(int b=;b<n;b++)
f[a][b]=1e+;
f[][]=;//初始化
//转移:较小的s=>较大的s,从小到大枚举s for(int s=;s<(<<n);s++)
for(int i=;i<n;i++)
if(((s>>i)&)==)
for(int j=;j<n;j++)
if(((s>>j)&)==)
f[s|(<<j)][j]=min(f[s|(<<j)][j],f[s][i]+dfs(i,j));
double ans=le+;
for(int a=;a<n;a++)
ans=min(ans,f[(<<n)-][a]); cout<<ans<<endl;
}

状压DP:

时间复杂度:O(n2*2n) 空间复杂度:O(2n*n)能接受的数据范围:n<=20(大约是,有时候可以达到22)

一般机子1s可以跑107~109看机子不同吧qwq

一些数据范围推复杂度和算法:

n≤12=>暴搜

n≤20(22)=>状压

n≤32  =>放弃

n≤50  =>放弃

n≤100 =>n3

n≤1000 =>n2

n≤105=>数据结构,线段树

n≤106=>线性的

n>106=>考虑O(1)的算法

在这普通的一天,我穿着普通的鞋,很普通地来到这普通的酒店,打开普通的电脑,找点普通的感觉,学一点我最爱的普通DP

普通DP

题目多一个条件=>多一个维度

first:

边界条件:

第一行和第一列全部为1:f[i][1]=f[1][j]=1;

转移:f[i][j]=f[i-1][j]+f[i][j-1];

上次zt钟神也讲的一个东西:

n行m列:从左上走到右下的方案数:Cn+m-2n-1

数字三角形:

洛谷p1216

每次可以向下或右下走。

这条路径的权值:这条路上所有数的和

求最大值

状态f[i][j]表示在走到(i,j)这个点最大的权值是多少;

边界条件:f[1][1]=a[1][1];

转移方程式:f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];

数字三角形改:

从a[1][1]向下或向右下走,使得路径权值mod m后的值最大

n,m<=100;

感性理解:

为什么不给大一点???

1.给大了做不了    m可能也是一个维度

2.出题人懒得给大数据(wz啊)

边界:f[1][1][a[1][1]]=true;

状态:f[i][j][k]走到i行j列%m==k是可能or不可能

转移:f[i][j][k]=f[i-1][j-1][k-a[i][j]]orf[i-1][j][k-a[i][j]]

注意k-a[i][j]可能会为负,故要取模

ans最后一行中可能出现的k中最大的

codevs 上的一道题

下一个问题:

最长上升子序列

不细讲了大家都做过qwq:

给定一个序列a1,a2,a3……an

找出一个序列b1,b2……bk使得:ab1<ab2<……abk

f[i]表示找一个以i号点结尾的最长上升子序列长度

转移:找一个j使得1<=j<i,aj<ai枚举f[i]=max f[j]+1;

复杂度O(n2

加强一下:数据范围n<=105

线段树qwq没学好啊

假设v=max(a1,a2,a3……,an)

建一棵大小为n的线段树(按值来做左右划分)

线段树:区间询问最大值&单点修改???

有点特殊的DP:背包问题

背包九讲

最基本背包问题:有n个物品,第i个物品价值为w[i],体积为v[i],现在有一个背包大小为m,在不超过背包容积情况下,价值最大。

01背包

状态定义:f[i][j]把前面i个物品是否放进背包已经考虑完了,背包中已经使用了j的体积的最大价值。

转移:如果不放进背包=>f[i][j]=>f[i+1][j] 0转移

如果放进背包=>f[i][j] +w[i+1]=>f[i+1][j+v[i+1]] 1转移

f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);

无穷背包(完全背包)

有n个物品,第i个物品价值为w[i],体积为v[i],每种物品都有无数个,问同上;

未优化:

转移方程:f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])O(nm)

存在转移链:

f[i][j]<=f[i][j-v[i]]<=f[i][j-2v[i]];

有限背包:

做法:枚举每个物品要用多少个(同无穷背包未优化的操作)

eg:求在[l,r]中满足各位数字之积为k的数有多少个

l,r<=1018

再加一维:f[i][j][k]填到i位,j=0  => “<”,j=1  => “=”,乘积为k;

k:max:918会炸,因为0~9中质因数只有2,3,5,7,故k=2a*3b*5c*7d

拆维度qwq拆的我一脸懵逼:

有的时候维度多并不代表会炸掉,有时反而或许可以降低复杂度qwq。

很神奇,你居然看到了最后,tql

那么接下来就是钟神语录了(算是看到最后的小彩蛋吧qwq)

"虽然我不知道你说的是什么,但肯定是错的"

“这个题我是从vijos看到的,不知道这个oj还活着没有”

“当你发现你这样做做不出来时,加一个维度,还做不出来,再加一个维度QWQ”

最新文章

  1. TensorFlow中权重的随机初始化
  2. 【CodeVS】 p1696 奇怪的函数
  3. 用Inno Setup来解决.NetFramework安装问题
  4. AppCan做的图片上传代码
  5. automaticallyAdjustsScrollViewInsets的作用
  6. MyBatis 一级、二级缓存
  7. Redis 图形化监控方案 RedisLive 介绍
  8. C语言循环的实现
  9. 巧用UserAgent来解决浏览器的各种问题
  10. 201621123040《Java程序设计》第13周学习总结
  11. [JSOI2008]Blue Mary开公司
  12. &#127827;JavaScript 对象原型链继承的弊端 &#127827;
  13. Python和Java编程题(六)
  14. linux下.bashrc文件 /PATH环境变量修改 /提示符修改
  15. pythonweb服务器编程(二)
  16. 20165311 ch02 课下作业
  17. centos系统中perl进程病毒占用大量网络流量导致网络瘫痪的问题分析及解决方案
  18. SPSS-判别分析
  19. AJPFX平台:外汇的基本面分析
  20. select * 和 select 所有字段写出来 ,速度对比!

热门文章

  1. 【crontab】误删crontab及其恢复
  2. 8. ClustrixDB 监控
  3. LeetCode - 乘积最大子串
  4. POJ 2186 挑战 --牛红人 强连通分量——Tarjan
  5. sublime Text3中文字体错位问题解决办法
  6. sh_01_重复执行
  7. (56)Linux驱动开发之二
  8. (48)LINUX应用编程和网络编程之三Linux获取系统信息
  9. python相关遗漏知识点补充
  10. linux系统/var目录的作用