关于DP动规


今天学了动规,简单记录一下自己理解了的:(要不俺就忘了)

首先,啥是DP???

动态规划,其实就是组合子问题的解来解决整个问题的解,由于每个子问题他只判断一次,所以不会重复计算,那就很牛啊!!!

专业术语(复制加粘贴):

1、 阶段:把所给求解问题的过程恰当地分成若干个相互联系的阶段,以便于按一 定的次序去求解,过程不同,阶段数就可能不同.描述阶段的变量称为阶段变量。

2、状态:某一阶段的出发位置成为状态,通常一个阶段有多个状态。状态通常可以用一个或一组数来描述,称为状态变量。

3、决策:一个阶段的状态给定以后,从该状态演变到下一阶段某个状态的一种选 择(行动)称为决策。描述决策变量称决策变量

4、策略和最优策略:所有阶段的决策有序组合构成一个策略。 最优效果的策略叫最优策略。

5. 状态转移方程:前一阶段的终点就是后一阶段的起点,对前一阶段的状态作出某种决策, 产生后一阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称 为状态转移方程。

其实也没什么好说的,直接上题吧!

T1:从n个数中取出k个数,使得他们的和最大

解一:sort排序

这就没啥可讲的了,你就建一个cmp让sort从大到小排序,选出前n大的数相加,就好啦,上代码!

//和最大问题(sort) 
#include
#include
using namespace std;
int a[1000000];//我不知道数据 
int sum;
int cmp(int x,int y)
{
    return x>y;
}
int main()
{
    int n,k;//n是总个数,k是取出的数的个数 
    cin>>n>>k;
    for(int i=0;i)
    {
        cin>>a[i];
    }
    sort(a,a+n,cmp);
    for(int i=0;i)
    {
        sum+=a[i];
    } 
    cout<<sum;
    return 0;
}

解二:DP

用动规来解决,首先要想出状态转移方程(好像有点难很难)

我们首先定义一个二维数组f[i][j],前面的表示第几个数(i),后面表示已经选取了几个数(j)

那么我们分将其成两种情况:我选或不选当前的数,

当我选取当前数时,它们的和就是上次的结果加上这个数,上次的结果就是f[i-1][j-1](因为目前的数是i,所以上一个的考虑到的数就是i-1,又因我考虑了这个数,所以上个数就是j-1),当前的数是a[i]。那我们如果加上这个数,那么现在所选数之和就是 f[i-1][j-1]+a[i]

那么如果不选前面的数时所得和其实就是选取上一个数时的和,即 f[i-1][j]

所以我们就可以得出其状态转移方程:f[i][j]=max( f[i-1][j-1]+a[i],f[i-1][j])

其他就好说了,具体如下

 1 #include
 2 #define lxl long long
 4 #define db double
 5 using namespace std;
 6 int f[10005][10005];
 8 int a[N];
 9 int n,k; 
11 int main(){
12     cin>>n>>k;
13     for(int i=1;i<=n;i++)
14      cin>>a[i];
15     for(int i=1;i<=n;i++){
16         for(int j=1;j<=k;j++){
17             f[i][j]=max(f[i-1][j-1]+a[i],f[i-1][j]);
18         }
19     }
20     cout<<f[n][k];
21     return 0;
22 }

T2.关于线性DP

从n个数中找出最长上升子序列(可以不连续),输出其长度

解法一、dfs搜索(坑)

 1 #include
 2 #include
 3 #define ll long long
 4 int n,a[1001],ans=0;
 5 void dfs(int c,int now,int len)
 6 {
 7     if(c==n)
 8     {
 9         if(len>ans) ans=len;
10         return;
11     }
12     for(int i=c;i<=n;i++)
13     {
14         if(a[i]>now) dfs(i,a[i],len+1);
15     }
16 }
17 int main()
18 {
19     scanf("%d",&n);
20     for(int i=1;i<=n;i++)
21     {
22         scanf("%d",&a[i]);
23     }
24     dfs(1,-1,0);
25     printf("%d",ans);
26     return 0;
27 }

我就提一句,dfs中c代表的是当前状态,即目前处理到的数,now代表前一个采用了的数,ans代表当前状态的上升序列长度,最后比较每一个ans,找出最大的即可

但是很遗憾,人家太慢了。就过了两个点。。。

那我还是讲讲其他的方法吧

解法二、记忆化搜索

不算很难

直接上核心代码理解一下吧,毕竟人家不是咱的重点关注对象

f[1][1]=a[1][1];
for(int i=2;i<=n;i++){
    for(int j=1;j<=i;j++)
     f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
}
int ans=0;
for(int i=1;i<=n;i++)
 ans=max(ans,f[n][i]);
cout<


正统解法,解法三:DP

用倒推正推都可以

①正推(其实就是 fk[x] =min{ fk-1[yi] +d [yi,x]}的一个载体)

 1 #include
 2 #define ll long long
 3 using namespace std;
 4 int n,a[1001],f[1001],p[1001];
 5 int main()
 6 {
 7     scanf("%d",&n);
 8     for(int i=1;i<=n;i++)
 9     {
10         scanf("%d",&a[i]);
11     }
12     f[1]=1;
13     for(int i=2;i<=n;i++)
14     {
15         f[i]=0;
16         for(int j=1;j)
17         {
18             if(a[j]f[i])
19             {
20                 f[i]=f[j];
21                 p[i]=j;
22             }
23         }
24         f[i]++;
25     }
26     int ans=f[1],k=1;
27     for(int i=2;i<=n;i++)
28     {
29         if(f[i]>ans)ans=f[k=i];
30     }
31     printf("%d",ans);
32     return 0;
33 }

啥意思呢,是这样的

 首先我们要明确我们定义的三个数组分别代表着啥:

a数组代表我输入的数组,f[i]数组代表i对应的最长子上升子序列长度,p数组用于表示f[i]的最优值j的位置上,就是它的上一个既满足在它前面,又比他小的数,然后我们可以举个例子:

比如 :                       1         4        3        

首先 i 等于2,也就是从第2个数 4 开始,我们让f[1]等于1(等会你就知道为啥了),再用一个 j ,他的范围是 [1,i),也就是找 i前面的数,那这里由于jf[i],那我们就让i的长度变成j的长度,并让p[i]=j,结束后 i 变成3,j从0到1开始遍历,是0的时候,a[j]代表1,a[i]等于3,由于满足a[j]f[i](==0),咱就进行一下操作:让f[i]=f[j]=1,i的前驱(q[i])=j=1;再轮到j=2,由于不满足a[j]

为啥要这样??

原因是我们要找的是最长上升子序列,所以前面的数a[j]必须比后面的数a[i]大(a[j]

p[i]=j 表示的是 i 的上一个即在它前面有比他小的数是 j(类似于它的前驱),我们把他记录下来,目的是便于最后输出。

最后让f[i]的长度加1,因为我们这些数每遇到一个就会加1,殊不知其实一开始只有一个数的时候长度为0,所以最后加1才是最终的长度。

倒序也一样:( fk[x ]= min{ fk+1[yi]+d [x,yi]})

 1 //最长上升子序列(逆推) 
 2 #include
 3 #include
 4 using namespace std;
 5 int a[1001];
 6 int f[1001];//对应的最长上升子序列长度 
 7 int p[1001];
 8 int ans;
 9 //p数组用于记录 f[i]的最优值j的位置 ,就是它的上一个既满足在他前面有比他小的数, 
10 int main()
11 {
12     int n;
13     cin>>n;
14     for(int i=1;i<=n;i++) cin>>a[i];
15     f[n]=1;p[n]=0;
16     for(int i=n-1;i>0;i--)
17     {
18         f[i]=0;
19         for(int j=i+1;j<=n;j++)
20         {
21             if(a[i]<=a[j]&&f[j]>f[i])
22 //两个条件:1.j对应的数比i大(保证上升)2.j的长度要大于i的长度,要不换他干啥
23             {
24                 f[i]=f[j];//让i的长度变成j的长度
25                 p[i]=j;//记录下来他的上一个即在它前面,又比他小的数,方便最后输出 
26             } 
27         }
28         f[i]++; 
29     } 
30     ans=f[1];
31     int k=1;
32     for(int i=2;i<=n;i++)
33     {
34         if(f[i]>ans) ans=f[k=i];//好高级 
35     }
36     cout<//这是最长上升子序列长度
37     while(k>0)
38     {
39         cout<" ";
40          k=p[k];
41     } 
42     return 0;
43 }

道理跟正推就是一样的

说了这么多了,总结一下吧

其实不难发现,我们会把一个大问题分成无数个小问题,但是他有一个前提。就是子问题的局部最优会导致整个问题的全局最优,也就是说无论过去我是咋决策的,现在的最优决策都是由原来的子问题的最优决策构成的,那也就是说一旦我们解决了子问题的最优,那以后的较大规模的问题也就迎刃而解了,而且一个问题的最优解只会受上一个最优子问题的影响,其他的子问题非最优解对现在的问题没有任何影响!

这就是老师讲的最优化原理无后效性

那我就思考了一下我刚才提及到的最长上升子序列是怎么体现出这个特点的呢??(仅个人观点)

仔细想想,其实我一开始就记录了每一个数的前驱,即它的上一个既满足在它前面,又比他小的数。我虽然全都记录下来了,但是我们只将最后我们得出的最长上升子序列的每个元素和其对应的前驱输出,而不是把每个元素对应的前驱全都输出,也就是错误的(不满足条件的)我就不输出了,这就保证了我的没满足全局最优的数并不会影响我们最后满足条件的数列,这就满足了DP的基本特征(条件),即无后效性,所以才可以用DP来做这道题。

还有就是,做这道题的思路是这样的:

1、划分阶段(在最长上升子序列一题中就是指的 i 和 j 对应的每一个阶段吧)

2、确定状态和变量(在这一题中我们运用了三个数组,分别表示  a[]原始数列、f[i] :i对应的最长子序列长度、g[i]: f[i]的最优值j的位置 ,就是它的上一个既满足在他前面有比他小的数)

3、确定状态转移方程

4、找出边界条件(这里就是 i 到头就行)

T3、坐标DP

一般就是个二维数组,,,

比如这道摘花生的题,说是一片地,一些坐标上有花生,现在你在左上角,只能向右或向下移动,问你一路上能摘多少花生?

我们肯定要先建一个二位数组记录每个点花生的数量(没有那肯定就是0啊),那我现在就想写出状态转移方程,怎么思考?

反正我到一个点要不就是从上面下来要不就是从左边往右走,那我们就比较上面的摘花生个数和左面总共摘得的花生个数,取最大值在加上这个点摘得的花生数量就是到这个点后你所最多能摘的花生数,即

f[i,j]=max{  f[i-1 , j],f[ i , j-1] } + a[i,j]

这样再用一个递推就能实现了。那我们想,我们所得到的目前阶段的最大值,肯定是不会受非最优子阶段的影响。所以是可以用DP做的。

其实还有很多很多很难很难的题,小生孤陋寡闻,才疏学浅,这里就不再体现,会查缺补漏,日臻完善的!!

我就记录到这了,再见!!

 2022/3/17

相关