动态规划
简单动态规划
前置知识
- 英文缩写
- 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;
}