动态规划

简单动态规划


前置知识

  • 英文缩写
  • e.g. 例如
  • etc. 等等
  • P.S. 备注

硬币问题

您有无限多的硬币,面值为1,5,10,20,50,100
给定一个数额w,问您最少用多少枚硬币可以凑出w

  • 贪心
    • 先尽量用100的,然后尽量用50的……以此类推.
    • e.g. 666 = 6100+150+110+15+1*1,共用10枚硬币.
    • 贪心是一种只考虑眼前情况的策略.
    • 尽管这一套硬币面值可以采用贪心策略,但是迟早要栽跟头的.
      • 新的硬币面值:1,5,11.
      • 如果我们要凑出15,贪心策略是:15 = 11+4*1,共用5枚硬币.
      • 而最佳策略是:15 = 3*5,共用3枚硬币.
  • 性质
    • w=15时,我们取了11,接下来面对w=4的情况
    • w=15时,如果我们取5,接下来就面对w=10的情况
    • 我们记“凑出n需要用到的最少硬币数量”为f[n].
    • w=15时,选cost最低那个
      • 11:cost=f[4]+1=4+1=5
      • 5:cost=f[10]+1=2+1=3
      • 1:cost=f[14]+1=4+1=5
    • 性质:f[n]只于f[n-1],f[n-5],f[n-11]相关
    • 更确切的说:f[n]=min{f[n-1],f[n-5],f[n-11]}+1
  • 代码
#include <bits/stdc++.h>

using namespace std;

int w;
int f[1005];

int main(){
	cin >> n;
	/******************************************/
	//push(递推)型结构	
	for(int i = 1; i <= n; ++i) f[i] = 0x7fffffff;
	for(int i = 0; i <= n; ++i){
		f[i + 1] = min(f[i + 1], f[i] + 1);
		f[i + 5] = min(f[i + 5], f[i] + 1);
		f[i + 11] = min(f[i + 11], f[i] + 1);
	}
	//pull(递归)型结构
	for(int i = 1, cost; i <= n; ++i){
		cost = 0x7fffffff;
		if(i >= 1) cost = min(cost, f[i - 1] + 1);
		if(i >= 5) cost = min(cost, f[i - 5] + 1);
		if(i >= 11) cost = min(cost, f[i - 11] + 1);
		f[i] = cost;
	}
	/******************************************/
	cout << f[n];
	return 0;
}

硬币问题原理

  • f[n]只与f[n-1],f[n-5],f[n-11]相关

  • 我们只关心f[w]的值,不关心是怎么取到w的

  • 可见,此做法比暴力快,因为我们舍弃了冗余的信息;我们只记录了对解决问题有帮助的信息-f[n]

  • 我们能这样干,取决于问题的性质:求出f[n],只需要知道几个更小的f[c].

  • 我们将求解f[c]称作求解f[n]的“子问题”.

  • 这就是DP(动态规划,dynamic programming).

  • 将一个问题拆成几个子问题,分别求解这些子问题,即可推断出大问题的解.


DP(动态规划,dynamic programming)

无后效性

一旦f[n]确定,“我们如何凑出f[n]”就再也用不着了。
要求出f[15],只需要知道f[14],f[10],f[4]的值,而f[14],f[10],f[4]是如何算出来的,对之后的问题没有影响。
“未来与过去无关”,这就是无后效性。

(严格定义:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响。)

最优子结构

回顾我们对f[n]的定义:
我们记“凑出n需要用到的最少硬币数量”为f[n].

f[n]的定义就已经蕴含了“最优”。
利用w=14,10,4的最优解,我们即可算出w=15的最优解。

大问题的最优解可以由小问题的最优解推出,这个性质叫做“最优子结构性质”。

DP条件

  • 大问题能拆成小问题
  • 满足无后效性
  • 满足最优子结构性质

DAG最短路

给定一个城市的地图,所有的道路都是单行道,而且不会构成环。每条道路都有过路费,问您从S点到T点花费的最少费用。

  • 记从S到P的最少费用为f[p]
  • f[T] = min(f[C] + 20, f[D] + 10);

代码



数字三角形

数字三角形

代码

#include<bits/stdc++.h>

using namespace std;

int n;

int a[1001][1001+10],dp[1001][1001+10];

int main(){
	
	cin>>n;
	
	for(int i=1;i<=n;++i){
		for(int j=1;j<=i;++j){
			cin>>a[i][j];
		}
	}
	
	for(int i=1;i<=n+1;++i) dp[n][i]=a[n][i];
	
	for(int i=n-1;i>=1;--i){
		for(int j=1;j<=i;++j){
			/*if(i+1==n/2&&(j==n/2||j+1==n/2)){
				dp[i][j]=a[i][j]+dp[n/2][n/2];
				continue;
			}*/
			dp[i][j]=max(a[i][j]+dp[i+1][j],a[i][j]+dp[i+1][j+1]);
		}
	}
	
	cout<<dp[1][1];
	
	return 0;
}

最大字段和

最大子段和
给出一个整数序列,问最大字段和。

  • e.g. [2,-1,3,-5,3]的最大字段和是2-1+3=4.
  • 要求一个O(n)的算法

代码

#include<bits/stdc++.h>

using namespace std;

const int N=200001;

int n,ans;
int a[N];
int S[N],A[N],dp[N];;//S前缀和 , A最小前缀和 

void doxx_1(){
    for(int i=1; i<=n; ++i){
		dp[i]=max(dp[i-1]+a[i],a[i]);
		ans=max(ans,dp[i]);
	}
}

void doxx_2(){
	int sum=0;
    for(int i=1;i<=n;++i){
        sum>=0?sum+=a[i]:sum=a[i];
        ans=max(ans,sum);
    }
}

void doxx_3(){
    for(int i=1;i<=n;++i) S[i]=S[i-1]+a[i];
    //for(int i=1;i<=n;++i) A[i]=min(A[i-1],S[i-1]);
    for(int i=1;i<=n;++i){
        A[i]=min(A[i-1],S[i-1]);
        ans=max(ans,S[i]-A[i]);
    }
}

int main(){ 
    cin>>n;
    for(int i=1;i<=n;++i) cin>>a[i];
    ans=a[1];
    doxx_1();
    //doxx_2();
    //doxx_3();
    cout<<ans;
    return 0;
}

最长上升子序列

给定长度为n的序列a,从a中抽取出一个子序列,这个子序列需要单调递增。问最长的上升子序列(LIS)的长度。

  • e.g. 1,5,3,4,6,9,7,8
  • LIS为1,3,4,6,7,8 长度为6

O(n^2)代码

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 1;

int n,ans;
int a[N],dp[N];

int main(){
	cin >> n;
	for(int i = 0; i < n; ++i) cin >> a[i], dp[i] = 1; 
	for(int i = 1; i < n; ++i){
		for(int j = 0; j < i; ++j){
			if(a[i] > a[j]) dp[i] = max(dp[i], dp[j] + 1);
		}
	}
	for(int i = 0; i < n; ++i) ans = max(ans, dp[i]);
	cout << ans;
	return 0;
}

O(nlogn)代码

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 1;

int n,len;
int a[N],f[N];

int find_up(int l, int r, int w){
	while(l < r){
		int m = (l + r) >> 1;
		if(f[m] >= w) r = m;
		else l = m + 1;
	}
	return l;
}		 

int main(){
	cin >> n;
	for(int i = 1; i <= n; ++i) cin >> a[i];
	for(int i = 1; i <= n; ++i){
		if(a[i] > f[len]) f[++len] = a[i];
		else{
			int j = find_up(1, len, a[i]);//寻找上升序列的拆入位置
			f[j] = a[i];
		}
	}
	cout << len;			 
	return 0;
}

导弹拦截代码

//寻找非严格单调递减和严格递增子序列
#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 1;

int n,len;
int a[N],dp[N];

int find_up(int l, int r, int w){	
	while(l < r){
		int m = (l + r) >> 1;
		if(dp[m] >= w) r = m;
		else l = m + 1;
	}
	return l;
}

int find_down(int l, int r, int w){	
	while(l < r){
		int m = (l + r) >> 1;
		if(dp[m] < w) r = m;
		else l = m + 1;
	}
	return l;
}

int main(){
	while(cin >> a[++n]); --n;
	dp[++len] = a[1];	
	for(int i = 2; i <= n; ++i){
		if(a[i] <= dp[len]) dp[++len] = a[i];
		else{
			int j = find_down(1, len, a[i]);
			dp[j] = a[i];
		}
	}
	cout << len << endl;
	len = 0;	
	for(int i = 1; i <= n; ++i){
		if(a[i] > dp[len]) dp[++len] = a[i];
		else{
			int j = find_up(1, len, a[i]);
			dp[j] = a[i];			
		}
	}
	cout << len;
	return 0;
}

原文地址:https://www.cnblogs.com/Adventurer-H/p/11304215.html