动态规划基础

基本概念

  • 动态规划是运筹学中⽤于求解决策过程中的最优化数学⽅法。
  • 动态规划过程是:每次决策依赖于当前状态,决策引发状态的转移。
  • 动态规划经常常使⽤于解决最优化问题,这些问题多表现为多阶段决策。
  • 状态:每个阶段所⾯临的条件,同⼀阶段可能会有不同的状态
  • 决策 (转移):对于⼀个阶段的某个状态,从该状态演变到下⼀阶段的某个状态的选择
  • 边界:决策过程中的初始情况
  • 策略:由每个阶段所做的所有决策组成的序列称为策略。所有可⾏策略中
  • 使得⽬标达到最佳情况的策略称为最优策略

当你企图使⽤计算机解决⼀个问题时,其实就是在思考如何将这个问题表达成状态(存储哪些数据)以及如何在状态中转移(怎样根据⼀些数据计算出另⼀些数据)。
所以所谓的空间复杂度就是为了⽀持你的计算所必需存储的状态最多有多少,所谓时间复杂度就是从初始状态到达最终状态中间需要多少步。

适用情况
  • 最优⼦结构:假设问题的最优解所包括的⼦问题的解也是最优的,就称该问题具有最优⼦结构,即满⾜最优化原理。
  • ⽆后效性:即某阶段状态⼀旦确定。就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响曾经的状态。仅仅与当前状态有关
一个例子

假设你要⾛⼀个⽹格状迷宫,迷宫的某些地⽅是墙,给定了起点和终点,只能往下或往右⾛,问最少需要多少步?

  • ⾸先考虑状态的表示。假设你和朋友在⼀起研究这个迷宫游戏,你要告诉他,你⾛这个迷宫到了某⼀个“状态”,那么要传达哪些信息呢?
  • F(x, y)=k,(x,y)表示坐标,k表示⼀个步数。那么你的朋友可以根据这些信息,完全得知你的游戏状态。
  • 我们考察⼀下这个设计是否满⾜动态规划适⽤情况。
  • ⽆后效性。可以理解成,状态之间满⾜⼀种拓扑序关系,之后⾛迷宫的进程,不会影响到之前的某个状态,或者说之前的状态不依赖于之后的决策。
  • 最优⼦结构。F(x,y)=min(F(x-1,y),F(x,y-1) + 1)
时间复杂度

往往可以通过增加状态的维数,记录更多的关键信息来满⾜⽆后效性与最优⼦结构,但维数的增加会导致重叠⼦问题减少⽽影响时间效率

  • 复杂度 = 状态数 × 决策数⽬ × 转移代价
  • 复杂度 = 实际状态数 + 总转移费⽤

序列型dp

本类的状态是基础的基础, ⼤部分的动态规划都要⽤到它, 成为⼀个维
⼀般来说, 有两种编号的状态

  1. 状态[i]表示前 i 个元素决策组成的⼀个状态
  2. 状态[i]表示⽤到了第i个元素,和其他在1到i−1间的元素,决策
    组成的⼀个状态,例如floyd

同样有状态、转移⽅程、⽆后效性等特点但⽆最优性决策过程
的递推往往也纳⼊动态规划的研究范围,如计算⽅案数

最长上升子序列

给出⼀个序列a1,a2,…,an,找到序列a的⼦序列a(i1),a(i2),…,a(il),1<=i1<i2<…<il<=n,a(i1)<a(i2)<…<a(il),使得l最⼤

问题分析
  • 状态:f(i)表⽰序列a1,a2,…,ai的包含ai的最长的⼦序列的长度
  • 阶段:按i从⼩到⼤划分阶段,即按照i从⼩到⼤的顺序计算f(i),计算f(i)时只依赖于前⾯计算过的f值
  • 决策:枚举f(i)对应的⼦序列a(i1),a(i2),…,a(i(l-1)),i中i(l-1)的值,设i(l-1)=j,则问题转化为在a1,a2,…,aj中找最长上升⼦序列。

状态转移⽅程:f(i)=max{f(j)+1},(1<=j<i,aj<ai)
时间复杂度:O(n^2)
可以⽤树状数组优化到 O(nlogn)

代码

O(n*n)

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int INF=1e9+7;

int a[N],dp[N]; // a数组为数据,dp[i]表示以a[i]结尾的最长递增子序列长度

int main()
{
    int n;
    while(cin>>n)
    {
        for(int i=0; i<n; i++)
        {
            cin>>a[i];
            dp[i]=1; // 初始化为1,长度最短为自身
        }
        int ans=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);  // 状态转移
                }
            }
            ans=max(ans,dp[i]);  // 比较每一个dp[i],最大值为答案
        }
        cout<<ans<<endl;
    }
    return 0;
}


O(nlogn) 贪心+二分

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int INF=1e9+7;

int a[[N],dp[N]; // a数组为数据,dp[i]表示长度为i+1的LIS结尾元素的最小值

int main()
{
    int n;
    while(cin>>n)
    {
        for(int i=0; i<n; i++)
        {
            cin>>a[i];
            dp[i]=INF; // 初始化为无限大
        }
        int pos=0;    // 记录dp当前最后一位的下标
        dp[0]=a[0];   // dp[0]值显然为a[0]
        for(int i=1; i<n; i++)
        {
            if(a[i]>dp[pos])    // 若a[i]大于dp数组最大值,则直接添加
                dp[++pos] = a[i];
            else    // 否则找到dp中第一个大于等于a[i]的位置,用a[i]替换之。
                dp[lower_bound(dp,dp+pos+1,a[i])-dp]=a[i];  // 二分查找
        }
        cout<<pos+1<<endl;
    }
    return 0;
}


数字三角形

N层数字三⻆形, N<=15,从顶部出发,在每⼀结点可以选择向左⾛或得向右⾛,⼀直⾛到底层。
要求找出⼀条路径,使路径上的值最⼤。

问题分析

这是一道典型的动态规划问题,求顶到底的一条路径数字总和最大,按照动态规划思想,从底往上,子问题的和大,那么父问题的和就大,所以在给数组打底子的时候,就要找大的

  1. 二位数组代表什么:b[i][j]代表从这个点一直走到最后的最大值
  2. 数组初始化:b数组把n-1行填充起来
  3. 递推式:根据题目可知,要从n-2行往0行遍历,也就是从下往上。公式为:b[m][n]=max{ b[m+1][n]+a[m][n] , b[m+1][n+1]+a[m][n] }

划分型dp

数的划分

将整数n分成k份,且每份不能为空,任意两种划分方案不能相同(不考虑顺序)。问有多少种不同的分法。

问题分析

f[i][j]是指将i划分成j部分的方案数, 初始化f[0][0]=1.
f[i][j]有意义(i>=j)的情况下满足公式 f[i][j]=f[i-1][j-1]+f[i-j][j]

分为两种情况 一种情况分出来的数中含1 另一种情况分出来的数中不含1

  1. 分离出一个1,那么剩余的i-1就只能划分成j-1个部分
  2. 不包含1 为了不包含1把最终所划分的j组每组都放上一个1,由于不能划分出0,因此不论剩下的数怎么划分,j组中都不可能有1.即将剩下的i-j分为j组
代码
/*
author:Manson
date:2019.
theme:
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> P;
const int N = 1005;

int dp[205][7];

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	int n,m;
	cin>>n>>m;
	dp[0][0]=1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if(i>=j)dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
        }
    }
    cout<<dp[n][m]<<endl;
	return 0;
}

抄书问题

现在要把M本有顺序的书分给K个人复制(抄写),每一个人的抄写速度都一样,一本书不允许给两个(或以上)的人抄写,分给每一个人的书,必须是连续的,比如不能把第一、第三、第四本书给同一个人抄写。现在请你设计一种方案,使得复制时间最短。复制时间为抄写页数最多的人用去的时间。
输出描述:第i行表示第i个人抄写的书的起始编号和终止编号。K行的起始编号应该从小到大排列,如果有多解,则尽可能让前面的人少抄写。

问题分析

dp[i][j]表示第i本书需要j个人抄写。k本书为上一段的末尾,k+1 ~ i为一段。
dp[i][j] = min( dp[i][j],max( dp[k][j-1],sum[i]-sum[k] ) )其中sum为前缀和

代码
/*
author:Manson
date:2019.
theme:
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> P;
const int N = 10005;

int dp[105][105];
int sum[105],a[105];

struct node{
	int l;
	int r;
}s[105];

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	int n,m;
	cin>>n>>m;
	if(n == 0)return 0;
	for(int i = 0;i < 105;i++){
		for(int j = 0;j < 105;j++){
			dp[i][j] = N;
		}
	}
	for(int i = 1;i <= n;i++){
		cin>>a[i];
		sum[i] = a[i]+sum[i-1];
		dp[i][1] = sum[i];
	}
	
	for(int i = 1;i <= n;i++){
		for(int j = 2;j <= m;j++){
			for(int k = i-1;k >= 1;k--){
				dp[i][j] = min(dp[i][j],max(dp[k][j-1],sum[i]-sum[k]));
			}
		}
	}
    int ans = dp[n][m];
    int cnt = 0;
    int res = 0,b = n;
    for(int i = n;i >= 1;i--){
    	res += a[i];
    	if(res > ans){
    		res = a[i];
    		s[++cnt].l = i+1;
    		s[cnt].r = b;
    		b = i;
		}
	}
	if(cnt != 0){
		cout<<1<<" "<<max(1,s[cnt].l-1)<<endl;
		for(int i = cnt;i >= 1;i--){
			cout<<s[i].l<<" "<<s[i].r<<endl;
		}
	}
	if(cnt == 0){
		cout<<1<<" "<<n<<endl;
	}
	return 0;
}

棋盘型dp

方格取数

设有 N*N 的⽹格图 (N<=10) ,我们将其中的某些⽅格中填⼊正整数,⽽其他的⽅格中则放⼊0。某⼈从图的左上⻆的A点出发,可以向下⾏⾛,也可以向右⾛,直到到达右下⻆的B点。在⾛过的路上,他可以取⾛⽅格中的数(取⾛后⽅格中变为0)。某⼈从A到B⾛⼀次,求取得数最⼤的⽅案?
若此人从A点到B 点共走两次,试找出2条这样的路径,使得取得的数之和为最大。

问题分析

如果某人只走一次,那么用 dp[i][j] 表示从(1,1)走到(i,j)的最大方案数。状态转移方程为
dp[i][j] = max(dp[i-1][j] , dp[i][j-1] ) + a[i][j];

若是某人走两次,可以设dp[x1][y1][x2][y2]表示第一条路走到(x1,y1),第二条路走到(x2,y2)。那么状态转移方程为dp[x1][y1][x2][y2] = max( dp[x1][y1][x2][y2] , dp[x1-1][y1][x2-1][y2] , dp[x1-1][y1][x2][y2-1] , dp[x1][y1-1][x2-1][y2] , dp[x1][y1-1][x2][y2-1] )。注意当(x1,y1)和(x2,y2)重合时,只能加一次a[x1][y1];

代码
/*
author:Manson
date:2019.
theme:
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> P;
const int N = 10005;

int dp[12][12][12][12];
int a[12][12];

int Max(int a,int b,int c,int d,int e){
	return max(a,max(b,max(d,e)));
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	int n;
	cin>>n;
	int x,y,z;
	while(1){
		cin>>x>>y>>z;
		if(!x&&!y&&!z){
			break;
		}
		a[x][y] = z;
	}
	for(int x1 = 1;x1 <= n;x1++){
		for(int y1 = 1;y1 <= n;y1++){
			for(int x2 = 1;x2 <= n;x2++){
				for(int y2 = 1;y2 <= n;y2++){
					int add = a[x1][y1]+a[x2][y2];
					if(x1 == x2 && y1 == y2){
						add -= a[x1][y1];
					}
					dp[x1][y1][x2][y2] = Max(dp[x1][y1][x2][y2],dp[x1-1][y1][x2-1][y2],
					dp[x1-1][y1][x2][y2-1],dp[x1][y1-1][x2-1][y2],dp[x1][y1-1][x2][y2-1])+add;
				}
			}
		}
	}
	cout<<dp[n][n][n][n]<<endl;
	return 0;
}
/*
8
2 3 13
2 6 6
3 5 7
4 4 14
5 2 21
5 6 4
6 3 15
7 2 14
0 0 0
*/
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值