2016.4.2 讲课笔记(动态规划)
一.动态规划中的几个点
1.状态(类比于函数),也就是确定要得出最优解最少需要多少个变量,需要确保的是保证变量的数目尽量少,而且得出的最优解正确
2.状态转移(类比于递归的理解):状态转移需要一个递推方程,状态的初值--递推的边界,
因为递归的速度很慢 ,所以我们就把递归变成for循环,可以提高效率,但是for循环的顺序的处理是关键。
3.求解--数组(顺序),记忆化搜索
求解方法一;数组的递推:可以避免递归的慢速,数组的维数就是状态中的参数(所以需要考虑降维的问题),
这种实现方法,for循环的顺序也是关键
求解方法二:记忆化搜索;当求出每个状态的顺序很难确定,那么就用递归,这并不是一般的递归,一般的递归速度慢,就是因为做了大量的重复计算,
使用记忆化搜索,当做到重复计算的时候,就返回之前算过的值,而不是再进行计算,这就是记忆化搜索.(这需要多开一个数组,用空间换取时间)。
4.特征;最优子结构:就是考虑怎么把当前状态拆开的问题?
5.优化:
注意优化的代价一定要小,否则优化就没有什么意义。具体的优化因为题目而异。没有通用的方法。
6.对于动态规划题目的思路(这是重中之重):1)注意分析样例。
2)尝试使用多种途径描述状态.对于每个方法,看看是否方便转移和构建方程
3) 动态规划的题目没有框架,只能类比于之前的题目,但是没有模板
4)状态转移方程就是寻找当前状态与之前已建立状态的关系。
专题二,最大正方形,最大子矩阵系列题目
1. 洛谷 P1387 最大正方形
题目描述
在一个n*m的只包含0和1的矩阵里找出一个不包含0的最大正方形,输出边长。
输入格式:输入文件第一行为两个整数n,m(1<=n,m<=100),接下来n行,每行m个数字,用空格隔开,0或1.
输出格式:一个整数,最大正方形的边长
4 4 0 1 1 1 1 1 1 0 0 1 1 0 1 1 0 1
2
/*以每个点作为正方形的右下角点进行递推, 递推方程的推导于正确性:当前以ij为右下角的大正方形内,一定有i-1j,ij -1,i-1j-1三个正方形,那么这三个正方形的大小就影响着ij的大小,这三个正方形 会在某处的限制条件而达到最大,他们针对于ij来说可能一个方向上比ij大,但是ij却 没有那么大,说明在三个方向上有一个正方形受到限制,ij就受到限制,所以就有了 poi[i][j].ans=min(poi[i-1][j].ans,min(poi[i-1][j-1].ans,poi[i][j-1].ans))+1;, 三者取小后+1,因为三个正方形不可能一起都可以扩张,那么ij就可以扩张了。 */ #include<iostream> using namespace std; #include<cstdio> #define N 101 struct Poi{ int num,ans; }; Poi poi[N][N]; int n,m; void input() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) scanf("%d",&poi[i][j].num); for(int i=1;i<=n;++i) if(poi[i][1].num==1) poi[i][1].ans=1; for(int j=1;j<=m;++j) if(poi[1][j].num==1) poi[1][j].ans=1; } int maxx=1; void DP() { for(int i=2;i<=n;++i) for(int j=2;j<=m;++j) { if(poi[i][j].num==1) { poi[i][j].ans=min(poi[i-1][j].ans,min(poi[i-1][j-1].ans,poi[i][j-1].ans))+1; if(poi[i][j].ans>maxx) maxx=poi[i][j].ans; } } printf("%d\n",maxx); return ; } int main() { input(); DP(); return 0; }
状态压缩,因为每次更新至于上一层有关,所以可以把数组压缩到只有[2][maxn]这么大就可以了
2.codevs 1159 最大全0子矩阵
时间限制: 1 s
在一个0,1方阵中找出其中最大的全0子矩阵,所谓最大是指O的个数最多。
输入文件第一行为整数N,其中1<=N<=2000,为方阵的大小,紧接着N行每行均有N个0或1,相邻两数间严格用一个空格隔开。
输出文件仅一行包含一个整数表示要求的最大的全零子矩阵中零的个数。
5
0 1 0 1 0
0 0 0 0 0
0 0 0 0 1
1 0 0 0 0
0 1 0 0 0
9
状态压缩版代码:
/*分析:1.求最大矩形不能类似于最大正方形的原因,正方形面积一定,形状也会一定,那么每次扩张的方式不变;对于同样面积的矩形来说, 不同的形状和,它的扩张方式和结果都是不同的,如果再记录下矩形的形状,就会增加空间复杂度,优势就没有了。 那么只能改变方法了。 2.分析;如果a[i][j]在矩形最后一行内,那么它向上走的0的数目,一定会大于等于长方形的宽。 l[i][j],r[i][j]记录从ij向左向右分别能扩的左边界和右边界,r-l+1起码大于等于长方形的长。 3.为什么要采用一个l,一个r?而不是仅仅一个表示左下或者右下角? 因为一个全1矩阵限制条件很多,他的一行当中有一个0,那么下一行的ij求面积时,就会放弃这一行。 而可能存在,比较扁的长方形中包含着比较高的正方形的一部分,只有l与r并用,才能把这个范围内,所有的可能组成的矩形,都求一遍,可以避免有的矩形面积求不到的情况。 4.左右扩张中的技巧;求左边界:定义mx=1,每次左边界对mx和i-1j的左边界,取大,if(当前点是1),就把mx=j+1,mx储存着下一个第一个0的位置,每次碰到1更新。 求右边,每次对mx取小,就可以了。 */ #include<iostream> #include<cstdio> using namespace std; #define INF 2001 int f[INF][INF],n,h[INF],l[INF],r[INF]; void input() { scanf("%d",&n); for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) { scanf("%d",&f[i][j]); } int ma=0; for(int i=1;i<=n;++i) { for(int j=1;j<=n;++j)/*状态压缩就在这,i是当前这一层,h[],r[],l[]是当前这一层的数据,那么顺便把这一行的点为矩形的最后一行的面积,更新最大值*/ { if(!f[i][j]) h[j]++; else h[j]=0; } for(int j=1;j<=n;++j) { l[j]=j; while(l[j]>1&&h[j]<=h[l[j]-1]) l[j]=l[l[j]-1]; } for(int j=n;j>=1;--j) { r[j]=j; while(r[j]<n&&h[j]<=h[r[j]+1]) r[j]=r[r[j]+1]; } for(int j=1;j<=n;++j) if(ma<h[j]*(r[j]-l[j]+1)) ma=h[j]*(r[j]-l[j]+1);/**/ } printf("%d\n",ma); } int main() { input(); return 0; }
3.codevs 1259 最大正方形子矩阵
时间限制: 1 s
在一个01矩阵中,包含有很多的正方形子矩阵,现在要求出这个01矩阵中,最大的正方形子矩阵,使得这个正方形子矩阵中的某一条对角线上的值全是1,其余的全是0。
第一行有两个整数n和m(1<=n,m<=1000)。接下来的n行,每行有m个0或1的数字。每两个数字之间用空格隔开。
只有一个整数,即这个满足条件的最大的正方形子矩阵的边长。
4 6
0 1 0 1 0 0
0 0 1 0 1 0
1 1 0 0 0 1
0 1 1 0 1 0
3
代码:
/*分析;第三道题是前两道题的集合:同时考虑:一个子矩阵,两个方向上的连续的0的数目,才能得到最优值*/ /*基本思路:统计每个点左上右各有多少个0(除自身以外),找最大正 方形子矩阵的时候,就以值为1的点,判断 他的左上(右上),上,左(右)各有多少个0,取一个小数后 加1,就是以当前这个点为左下角或者右下角的正方形的最大边长。 想法;因为题目中的1对角线是最难处理的,所以就把这个1作为突破口*/ #include<iostream> using namespace std; #include<cstdio> #define N 1001 int n,m; struct Poi{ int l,r,num,ans,up; }; Poi poi[N][N]; int maxx=-N; void update() { for(int i=2;i<=n;++i)/*分别统计左上右各有多少个0*/ for(int j=1;j<=m;++j) { if(poi[i-1][j].num==0) poi[i][j].up=poi[i-1][j].up+1; } for(int j=2;j<=m;++j) for(int i=1;i<=n;++i) { if(poi[i][j-1].num==0) poi[i][j].l=poi[i][j-1].l+1; } for(int j=m-1;j>=1;--j) for(int i=1;i<=n;++i)/*注意不同的寻找for循环的顺序是不同的*/ { if(poi[i][j+1].num==0) poi[i][j].r=poi[i][j+1].r+1; } } void input() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) { scanf("%d",&poi[i][j].num); } update(); } void countz()/*求左上方的正方形的最大边长*/ { for(int i=1;i<=m;++i) if(poi[1][i].num==1) poi[1][i].ans=1; for(int i=1;i<=n;++i) if(poi[i][1].num==1) poi[i][1].ans=1; for(int i=2;i<=n;++i)/*注意不同的寻找for循环的顺序是不同的*/ for(int j=2;j<=m;++j) { if(poi[i][j].num==1) poi[i][j].ans=min(min(poi[i][j].l,poi[i][j].up),poi[i-1][j-1].ans)+1; if(poi[i][j].ans>maxx) maxx=poi[i][j].ans; } } void county() { for(int i=1;i<=n;++i) poi[i][m].ans=1; for(int i=2;i<=n;++i) for(int j=m-1;j>=1;--j) { if(poi[i][j].num==1) poi[i][j].ans=min(min(poi[i][j].r,poi[i][j].up),poi[i-1][j+1].ans)+1; if(poi[i][j].ans>maxx) maxx=poi[i][j].ans; } } int main() { input(); countz(); county(); printf("%d\n",maxx); return 0; }
专题三;数的划分总结性题目
1.NOI 666:放苹果
- 总时间限制:
- 1000ms
- 内存限制:
- 65536kB
- 描述
- 把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用K表示)5,1,1和1,5,1 是同一种分法。
- 输入
- 第一行是测试数据的数目t(0 <= t <= 20)。以下每行均包含二个整数M和N,以空格分开。1<=M,N<=10。
- 输出
- 对输入的每组数据M和N,用一行输出相应的K。
- 样例输入
-
1 7 3
- 样例输出
-
8
代码;
/*解题分析: 设f(m,n) 为m个苹果,n个盘子的放法数目,则先对n作讨论, 当n>m:必定有n-m个盘子永远空着,去掉它们对摆放苹果方法数目不产生影响。 即if(n>m) f(m,n) = f(m,m) 当n<=m:不同的放法可以分成两类: 1、有至少一个盘子空着,即相当于f(m,n) = f(m,n-1); 因为 f(m,n-1)是 由f(m,n-2)推过来的,所以至少有一个盘子空着 2、所有盘子都有苹果,相当于可以从每个盘子中拿掉一个苹果, 不影响不同放法的数目, 即f(m,n) = f(m-n,n). 而总的放苹果的放法数目等于两者的和,即 f(m,n) =f(m,n-1)+f(m-n,n) */ //a[i][j]表示把i个苹果放到j个盘子里 #include<iostream> using namespace std; #include<cstdio> int p[11][11]; #include<cstring> int n,m,t; int main() { scanf("%d",&t); while(t--) { scanf("%d%d",&m,&n); memset(p,0,sizeof(p)); for(int i=1;i<=m;++i) p[i][1]=1; for(int i=1;i<=n;++i) { p[1][i]=1; p[0][i]=1;/*应对i==j,当i==j的时候放的方法是1,i-j=0,所以这么赋值*/ } for(int i=2;i<=m;++i)//shu for(int j=1;j<=n;++j)//fen { if(i<j) p[i][j]=p[i][i]; else p[i][j]=p[i][j-1]+p[i-j][j]; } printf("%d\n",p[m][n]); } return 0; }
2.数的划分DP实现:
以前做的数的划分,我都是用递归做,今天遇上一个复杂的数的划分,结果以前的方法就不太好了,所以在这里总结一下,多种数的划分题目
NOI 8787:数的划分
- 描述
-
将整数n分成k份,且每份不能为空,任意两份不能相同(不考虑顺序)。
例如:n=7,k=3,下面三种分法被认为是相同的。
1,1,5; 1,5,1; 5,1,1;
问有多少种不同的分法。 输出:一个整数,即不同的分法。
- 输入
- 两个整数n,k (6 < n <= 200,2 <= k <= 6),中间用单个空格隔开。
- 输出
- 一个整数,即不同的分法。
- 样例输入
-
7 3
- 样例输出
- 4
- 提示
- 四种分法为:1,1,5;1,2,4;1,3,3;2,2,3。
- 来源
- NOIP2001复赛 提高组 第二题
- 代码:
#include<iostream> using namespace std; #include<cstdio> long long int f[201][10]; int n,k; int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;++i) for(int j=1;j<=i&&j<=k;++j) { if(j==1) f[i][j]=1; else f[i][j]=f[i-1][j-1]+f[i-j][j]; /*因为一定有n>k,而且注意题目中说的每份不为空,那么至少是1, 当n>=k:不同的取法可以分成两类: 1、有没有一个盘子空着,那么至少取1,即相当于f(n,k) = f(n-1,k-1); 2、所有盘子都有苹果,相当于可以从每个盘子中拿掉一个苹果,不影响不同放法的数目 f(n,k) += f(n-k,k). */ } cout<<f[n][k]<<endl; return 0; } View Cod
3.对比数的划分与放苹果的动态转移方程
放苹果:
if(i<j) p[i][j]=p[i][i]; else p[i][j]=p[i][j-1]+p[i-j][j];/*油的盘子可以是空,所以可以有盘子不放,这就是p[i][j-1]*/
数的划分:
if(j==1)
f[i][j]=1;
else f[i][j]=f[i-1][j-1]+f[i-j][j];/*这里减去1的原因,取一个数的时候,不能取0,至少是1,所以为f[i-1][j-1]*/