本文由AlvinZH所写,欢迎学习引用,如有错误或更优化方法,欢迎讨论,联系方式QQ:1329284394。
前言
动态规划(Dynamic Programming),是一个神奇的东西。DP只能意会,不可言传。大家在做DP题的时候一定要理清思路,一般是先不管空间,毕竟以空间换时间,大多数题都是先卡时间再卡空间的。
DP具备的两个要素:最优子结构和子问题重叠,见《算法导论》225页。简单来讲就是问题是一个由多决策产生最优值的最优化问题。
- 最优化原理:其子问题的最优会导致全局最优,具有最优子结构的性质。这是运用DP的"前提",是否符合最优化原理是一个问题的本质特征。如果不满足最优化原理,那最开始所做的决策都是徒劳的。
- 无后效性:当前状态如果确定,以后过程的演变将不再受当前状态以前的各状态和以前的决策影响。这是运用DP的"条件",DP按次序去求每阶段的解,如果一个问题有后效性,那么这样的次序便是不合理的。一个问题的某个DP决策方法可能具有后效性,通过重新划分阶段,重新选定状态,或者增加状态变量的个数等手段,是可以把问题转化为满足无后效性的。所以决策的"顺序"也是问题的关键。
接下来通过几道经典的题目,简单练习一下DP,比赛题目连接:BUAAOJ-DP大作战 H~M题。
899 AlvinZH掉坑里了(H)
思路
简单DP。简单判断符合运用DP要求,求得到达某个点的最大金币数,至多只要比较两点(左点&上点)的最大金币数,即满足最优子结构。
\(dp[i][j]\) :表示走到点(i,j)时取得的最大金币数。
状态转移方程: \(dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]) + M[i][j]\) 。
小技巧:①初始化为-INF;②真实数据存于[1~n][1~m]中,边缘统一。
参考代码
//
// Created by AlvinZH on 2017/10/17.
// Copyright (c) AlvinZH. All rights reserved.
//
#include <cstdio>
#include <cstring>
#define INF 0x3f3f3f3f
int M[505][505];//矩阵数据
int dp[505][505];//到达点(i,j)时最大金币个数
inline int MAX(int i, int j) {
if(dp[i - 1][j] > dp[i][j - 1]) return dp[i - 1][j];
else return dp[i][j - 1];
}
int main()
{
int n, m;
while(~scanf("%d %d", &n, &m))
{
memset(dp, -INF, sizeof(dp));
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
scanf("%d", &M[i][j]);
dp[1][1] = M[1][1];
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
if(dp[i][j] < 0) dp[i][j] = MAX(i, j) + M[i][j];
printf("%d\n", dp[n][m]);
}
}
/*
* 简单DP
* dp[i][j]表示走到点(i,j)时取得的最大金币数。
* 状态转移方程:dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]) + M[i][j]。
*/
900 AlvinZH又掉坑里了(I)
思路
难题。
错误思路:贪心。运用上一题的写法,先走一次,路径置零,再来一次,两次最大值相加。你会发现你样例都过不了(要是放个恰好满足的样例不知道要WA多少次)。仔细一想,两次最优加起来还会是最优吗?真不一定,看这题就知道了。
既然不能分两次处理,那就同步处理吧。如何同步呢?多路DP,即想象两个人同时从左上走到右下,保证在同一点只取一次,求两人最大金币数和。用四维数组dp[205][205][205][205]?看着就挺吓人的,不过简单易懂,状态转移方程也可以很快得出:dp[i][j][x][y]=max{dp[i-1][j][x-1][y],dp[i-1][j][x][y-1],dp[i][j-1][x-1][y],dp[i][j-1][x][y-1]},代表两人到达(i,j)和(x,y)时的最大金币数。虽然明知会MLE,这一步的思考是有必要的,因为这是优化的基础。
发现惊喜:上述状态转移方程四个决策中有 \(i+j=x+y\) ,故可以轻易的把四维降成三维。这里有两种方法优化:
- 第一种方法稍微作优化,需要dp[405][205][205]。其中dp[step][x][y]:表示第step步时(两人一起走),第一个人在第x行,第二个人在第y行的最大收益,答案为dp[m + n][n][n]。两人坐标为(x,step-x)、(y,step-y),两个人在同一行时,一定在同一列,需要注意走到同一点时的处理方法。状态转移如下,四种决策(下下,下右,右下,右右)去最优,具体见参考代码一。
//下下,下右,右下,右右四者取最大值
dp[i][j][k] = MAX(dp[i-1][j-1][k-1], dp[i-1][j][k-1], dp[i-1][j-1][k], dp[i-1][j][k]);
if (j == k)//走到同一行,必定在同一列,所以确定到达A[j][i - j]同一点
dp[i][j][k] += M[j][i-j];
else//走到不同行,所以确定到达A[j][i-j]、A[k][i-k]两点。
dp[i][j][k] += (M[j][i-j] + M[k][i-k]);
- 第二种方法优化更佳,也易懂,需要dp[205][205][205]。其中dp[i][j][k]表示第一个人走到(i,j),第二个人走到横坐标为k,由于两人一起走,可以算出第二人坐标为(k,i+j-k)。这里可以直接避免走到同一点,k!=i即可。状态转移方程如下,同样是取四种决策最优,具体见参考代码二。
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
for(int k = 1; k <= n && k <= (i+j); ++k)
{
int t = (i+j)-k;
if ( k != i )//保证不重复
dp[i][j][k] = M[i][j]+M[k][t]+MAX(dp[i-1][j][k],dp[i][j-1][k],dp[i-1][j][k-1],dp[i][j-1][k-1]);
}
这两种优化很相似,而第二种比第一种空间整整小了一倍,有人问为什么还要放在这里讨论,因为,第一种方法还可以继续优化,我们发现,在状态转移方程中,dp[i][][]只与dp[i-1][][]有关,这意味着什么?这意味着可以把第一维继续优化,即数组变为dp[2][205][205],采用滚动数组,把第一维循环利用。状态转移方程如下,具体可见参考代码三。
int cur = 0;
for (int i = 2; i <= n + m; i++) {
cur ^= 1;
for (int j = 1; j <= n&&i - j >= 0; j++) {
for (int k = 1; k <= n&&i - k >= 0; k++) {
//下下,下右,右下,右右四者取最大值
dp[cur][j][k] = MAX(dp[cur^1][j-1][k-1], dp[cur^1][j][k-1], dp[cur^1][j-1][k], dp[cur^1][j][k]);
if (j == k)//走到同一行,必定在同一列,所以确定到A[j][i - j]一点
dp[cur][j][k] += M[j][i-j];
else//走到不同行,所以确定到A[j][i-j]、A[k][i-k]两点。
dp[cur][j][k] += (M[j][i-j] + M[k][i-k]);//右右
}
}
}
三种方法评测记录对比如下:
参考代码一
//
// Created by AlvinZH on 2017/10/17.
// Copyright (c) AlvinZH. All rights reserved.
//
#include <cstdio>
#include <cmath>
#include <cstring>
#include <iostream>
using namespace std;
int m, n;
int M[201][201];
int dp[402][201][201];
inline int MAX(int a, int b, int c, int d) {
int minAns = a;
if(minAns < b) minAns = b;
if(minAns < c) minAns = c;
if(minAns < d) minAns = d;
return minAns;
}
int main()
{
while(~scanf("%d%d", &n, &m))
{
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &M[i][j]);
for (int i = 2; i <= n + m; i++) {
for (int j = 1; j <= n && i - j >= 0; j++) {
for (int k = 1; k <= n && i - k >= 0; k++) {
//下下,下右,右下,右右四者取最大值
dp[i][j][k] = MAX(dp[i-1][j-1][k-1], dp[i-1][j][k-1], dp[i-1][j-1][k], dp[i-1][j][k]);
if (j == k)//走到同一行,必定在同一列,所以确定到达A[j][i - j]同一点
dp[i][j][k] += M[j][i-j];
else//走到不同行,所以确定到达A[j][i-j]、A[k][i-k]两点。
dp[i][j][k] += (M[j][i-j] + M[k][i-k]);
}
}
}
printf("%d\n",dp[n + m][n][n]);
}
return 0;
}
参考代码二
//
// Created by AlvinZH on 2017/10/17.
// Copyright (c) AlvinZH. All rights reserved.
//
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
int m, n;
int M[201][201];
int dp[201][201][201];
inline int MAX(int a, int b, int c, int d) {
int minAns = a;
if(minAns < b) minAns = b;
if(minAns < c) minAns = c;
if(minAns < d) minAns = d;
return minAns;
}
int main()
{
while(~scanf("%d%d", &n, &m))
{
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
scanf("%d", &M[i][j]);
dp[1][1][1] = M[1][1];
for(int i = 1; i <= n; ++i)
for(int j = 1; j <= m; ++j)
for(int k = 1; k <= n && k <= (i+j); ++k)
{
int t = (i+j)-k;
if ( k != i )//保证不重复
dp[i][j][k] = M[i][j]+M[k][t]+MAX(dp[i-1][j][k],dp[i][j-1][k],dp[i-1][j][k-1],dp[i][j-1][k-1]);
}
printf("%d\n", dp[n][m-1][n-1] + M[n][m]);
}
}
参考代码三(最优)
//
// Created by AlvinZH on 2017/10/17.
// Copyright (c) AlvinZH. All rights reserved.
//
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int m, n;
int M[201][201];
int dp[2][201][201];
inline int MAX(int a, int b, int c, int d) {
int minAns = a;
if(minAns < b) minAns = b;
if(minAns < c) minAns = c;
if(minAns < d) minAns = d;
return minAns;
}
int main()
{
while(~scanf("%d%d", &n, &m))
{
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &M[i][j]);
//发现每一步只与前一步有关,可以滚动数组,把一维滚动掉。
int cur = 0;
for (int i = 2; i <= n + m; i++) {
cur ^= 1;
for (int j = 1; j <= n&&i - j >= 0; j++) {
for (int k = 1; k <= n&&i - k >= 0; k++) {
//下下,下右,右下,右右四者取最大值
dp[cur][j][k] = MAX(dp[cur^1][j - 1][k - 1], dp[cur^1][j][k - 1], dp[cur^1][j - 1][k], dp[cur^1][j][k]);
if (j == k)//走到同一行,必定在同一列,所以确定到A[j][i - j]一点
dp[cur][j][k] += M[j][i - j];
else//走到不同行,所以确定到A[j][i - j]、A[k][i - k]两点。
dp[cur][j][k] += (M[j][i - j] + M[k][i - k]);//右右
}
}
}
printf("%d\n",dp[cur][n][n]);
}
return 0;
}
901 AlvinZH双掉坑里了(J)
思路
简单DP。简化问题:将n个金币放入m个盒子,无空盒。
直接上dp吧,dp[i][j]:将i个金币放入j个盒子的方法数。此题的关键在于如何找到状态转移方程,很有可能会计算重复的方法。我们把答案分成两部分:
①放完之后所有盒子金币数量大于1;
②放完之后至少有一个盒子金币数量为1。
这样分可以保证不会有重复计算。状态转移方程: \(dp[i][j] = dp[i-j][j] + dp[i-1][j-1]\) 。
① \(dp[i-j][j]\) :将(i-j)个金币放到j个盒子,然后这j个盒子每个再放1个金币。表示的是将i个金币分成所有盒子金币数量大于1的方案总数。例如,求9分解成3份,6(9-3)分成3份可以分为{1,1,4}{1,2,3}{2,2,2},则9可以分为{2,2,5}{2,3,4}{3,3,3},共3种。
② \(dp[i-1][j-1]\) :将(i-1)个金币放到(j-1)个盒子,再来一个盒子放1个。表示的是将i个金币分成至少有一个盒子金币数量为1的方案总数。例如,求9分解成3份,8(9-1)分成2份可以分为{1,7}{2,6}{3,5}{4,4},则9可以分为{1,1,7}{1,2,6}{1,3,5}{1,4,4},共4种。
难点在于如何避免重复,这里处理得十分巧妙,请细细体会。
参考代码
//
// Created by AlvinZH on 2017/10/23.
// Copyright (c) AlvinZH. All rights reserved.
//
#include <cstdio>
#include <cstring>
#define MOD 1000007
int n, m;
int dp[10005][1005];
int main()
{
while(~scanf("%d %d", &n, &m))
{
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if(i - j >= 0)
dp[i][j] = (dp[i-j][j] + dp[i-1][j-1]) % MOD;
}
}
printf("%d\n", dp[n][m]);
}
}
902 AlvinZH叒掉坑里了(K)
思路
简单DP。与上一题十分相似,问题简化为:将n个金币放入至多m个盒子,不存在相等数量金币的盒子。
dp[i][j]:将i个金币放入j个盒子的方法数。本题同样可以沿用上一题思想,把答案分成两部分。但是有一个问题是不能有相同数量金币的盒子,如果像上一题一样处理,我们会出现多个1的情况,需要避免这些情况。
①放完之后所有盒子金币数量大于1;
②放完之后只有一个盒子金币数量为1。
这样分可以保证不会有重复计算,而且不会有相同。状态转移方程: \(dp[i][j] = dp[i-j][j] + dp[i-j][j-1]\) 。
① \(dp[i-j][j]\) :将(i-j)个金币放到j个盒子,然后这j个盒子每个再放1个金币。表示的是将i个金币分成所有盒子金币数量大于1的方案总数。
② \(dp[i-j][j-1]\) :将(i-j)个金币放到(j-1)个盒子,然后这(j-1)个盒子每个再放1个金币,最后再来一个盒子放1个金币。表示的是将i个金币分成至少有一个盒子金币数量为1的方案总数。
对比上一题,状态转移方程仅仅差了一个字符
难点在于如何避免重复以及相同数目,这里处理得十分巧妙,请细细体会。
优化问题
本题需要注意内存限制,dp[50005][50005]是会MLE的。由于本题要求分成不同的数目,1+2+3+...+m=n,可以得到 \(m<sqrt(2_n)\) ,于是dp数组变成dp[50005][350]。时间复杂度为 \(O(n_sqrt(2n))\) 。具体见参考代码一。
与第二题相似,我们发现,dp[i][j]只与dp[][j]和dp[][j-1]有关,那么这里可以对空间再次优化,dp数组变为dp[50005][2],具体操作见参考代码二。真tm神奇啊~
参考代码一
//
// Created by AlvinZH on 2017/10/23.
// Copyright (c) AlvinZH. All rights reserved.
//
//正常写法
#include <cstdio>
#include <cstring>
#define MOD 1000007
int n;
int dp[50005][350];
int main()
{
while(~scanf("%d", &n))
{
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
int ans = 0;
for (int i = 1; i < 350; ++i) {
for (int j = 0; j <= n; ++j) {
if(j - i >= 0)
dp[j][i] = (dp[j-i][i] + dp[j-i][i-1]) % MOD;
}
ans = (ans + dp[n][i]) % MOD;
}
printf("%d\n", ans);
}
}
参考代码二(最优)
#include <cstdio>
#include <cstring>
#define MOD 1000007
int n;
int dp[50005][2];
int main()
{
while(~scanf("%d", &n))
{
memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
int ans = 0;
for (int i = 1; i < 350; ++i) {
for (int j = 0; j < 350; ++j)//每次操作初始化
dp[j][i&1] = 0;
for (int j = 0; j <= n; ++j) {
if (j - i >= 0)
dp[j][i&1] = (dp[j - i][i&1] + dp[j - i][(i - 1)&1]) % MOD;
}
ans = (ans + dp[n][i&1]) % MOD;
}
printf("%d\n", ans);
}
}
903 AlvinZH叕掉坑里了(L)
思路
难题。本题已经超越了dp,但其本质还是dp。简化题目:将一个数拆成一个或多个数的和,即无序整数拆分问题。
无序整数拆分问题是欧拉五边形数定理的一个应用。详情请查看:分拆数 && hdu 4651 && hdu 4658。
证明五边形数定理以及证明无序拆分整数是五边形数定理的应用,这。。。就超出我的知识范围了。
参考代码
//
// Created by AlvinZH on 2017/10/23.
// Copyright (c) AlvinZH. All rights reserved.
//
#include <cstdio>
#include <cstring>
#define MaxSize 50005
#define MOD 1000007
#define f(x) (((x) * (3 * (x) - 1)) >> 1)
#define g(x) (((x) * (3 * (x) + 1)) >> 1)
using namespace std;
int n, ans[MaxSize];
void init()
{
memset(ans, 0, sizeof(ans));
ans[0] = 1;
for (int i = 1; i <= 50000; ++i) {
for (int j = 1; f(j) <= i; ++j) {
if (j & 1)
ans[i] = (ans[i] + ans[i - f(j)]) % MOD;
else
ans[i] = (ans[i] - ans[i - f(j)] + MOD) % MOD;
}
for (int j = 1; g(j) <= i; ++j) {
if (j & 1)
ans[i] = (ans[i] + ans[i - g(j)]) % MOD;
else
ans[i] = (ans[i] - ans[i - g(j)] + MOD) % MOD;
}
}
}
int main()
{
init();
while (~scanf("%d", &n))
{
printf("%d\n", ans[n]);
}
}
/*
* 欧拉五边形定理:P(n)表示n的划分种数。
* P(n) = ∑{P(n - k(3k - 1) / 2 + P(n - k(3k + 1) / 2 | k ≥ 1}
* n < 0时,P(n) = 0;n = 0时, P(n) = 1即可。
*/
916 AlvinZH不想掉坑里了(M)
分析
中等题。单源最短路径。最短路径是一个经典算法问题,所以我为其特地单独写了一篇随笔,仅供参考。
AlvinZH又来骗访客量啦:四大算法解决最短路径问题。
参考代码
//
// Created by AlvinZH on 2017/11/3.
// Copyright (c) AlvinZH. All rights reserved.
//
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
const int N=100010;
const int INF = 0x3f3f3f3f;
bool Vis[N];//是否被访问过
int Dis[N];//距离
struct DisAndStart
{
int dis;//距离
int start;//起点
bool operator < (const DisAndStart& p)const {
return p.dis<dis;
}
DisAndStart(int d, int s):dis(d),start(s){}
};
vector<pair<int, int> > V[N];//二维的vector数组
void dijkstra(int s)
{
priority_queue<DisAndStart> Q;
memset(Dis,INF,sizeof(Dis));
memset(Vis,0,sizeof(Vis));
Dis[s]=0;
Q.push(DisAndStart(0,s));
while(!Q.empty())
{
DisAndStart p=Q.top();
Q.pop();
if(Vis[p.start]) continue;//已经访问过该点
Vis[p.start]=1;
for(int t=0;t<V[p.start].size();t++)
{
int end=V[p.start][t].first;
int Time=V[p.start][t].second;
if(Dis[p.start]+Time<Dis[end])
{
Dis[end]=Dis[p.start]+Time;
Q.push(DisAndStart(Dis[end],end));
}
}
}
}
int main()
{
//freopen("in2.txt", "r", stdin);
//freopen("out2.txt", "w", stdout);
int n, m, k, des;
int x, y, Time;
while(~scanf("%d%d%d", &n, &m, &k))
{
for(int i = 1; i <= n; i++)//清空数据
V[i].clear();
while(m--)
{
scanf("%d%d%d", &x, &y, &Time);
V[x].push_back(make_pair(y, Time));
V[y].push_back(make_pair(x, Time));
}
dijkstra(1);
int cnt = 1;
for(int i = 0; i < k; ++i)
{
scanf("%d", &des);
if(Dis[des] == INF) printf("Case %d:-1\n", cnt);
else printf("Case %d:%d \n", cnt, Dis[des]);
cnt++;
}
printf("\n");
}
}