ACM_普通DP

引言

DP: 即dynamic program 动态规划的意思, 这是一种用之前的状态推之后的状态的解决问题的方法, 也可以说用空间换时间 本文将以:
1.动态规划的状态, 状态转移, 初始化
2.动态规划的递推和递归
3.动态规划的例题
4.动态规划的一些技巧
来说明动态规划

为了更好的说明 先直接给个例题http://poj.org/problem?id=1163
ACM_普通DP_第1张图片
ACM_普通DP_第2张图片
题目大意: 给你一个数字三角形, 让你从顶部走到底部, 每次只能向左下或者右下走, 记录下你走的路的数字和. 问你能得到的最大数字是多少;


动态规划的状态, 状态转移, 初始化

  1. 动态规划的状态:
    是对于某个大问题的子问题的具体形式, 一般写在数组中的下标中, 它表示的是这个子问题所在的状态
  2. 状态转移:
    为了解决最后的大问题, 小问题会解决大一点的问题, 再解决更大一点的问题, 直到得到问题的解, 这种用小问题推出大问题的解得过程就是状态转移
  3. 初始化:
    初始化三两方面用处: 一.解决最小的问题, 二.因为状态转移中可能涉及到取大取小的情况, 一般取大的话可以初始成0 或者 -无穷 看情况而定, 取小的话就相反(当然也可以用一个取不到的数表示这个东西还没用, 最开始判断一下就可以了) 三. 可以设置非法状态, 对于某些题 有一些地方是不可取的, 这时候就需要把这些地方(状态)设置成一个绝对不会取到的值

    对于上面的例题: 这是动态规划的入门题目, 基本每次给别人动态规划入门的时候都会用这个题, 因为它的确是太简单, 太容易懂了!
    首先我们需要录入数据, 这时候我们是把它录入的数据应该是直角三角形的样子(因为数组不可能像图中三角行那么奇葩) 这样我们就是只能往下走和往右下角走. 得到的数组大概是这样
    ACM_普通DP_第3张图片
    这是我们存下的数组, 假设是这个二维数组叫a 那么a[0][0] = 7, a[1][0] = 3, a[1][1] = 8 ……
    为了解决这个问题, 我们可能最开始想到暴力, 直接算出所有情况, 取最大值(当然不行, 会T) 然后我们重新看, 假设我们走到了第三层, 正在向第四层走, 通过简单手算我们也能很容易的知道到达第三层每个位置的最优解 比如到a[2][0]的最优解是7 + 3 + 8, 到a[2][1]的最优解是7 + 8 + 1, 到a[2][2]的最优解是7 + 8 + 0, 我们发现, 再向下走的时候就和上面两层没关了, 就是说我们向第四层走的时候肯定在第三层的最优解里面选择 如果不同的话 举个例子 我们到a[3][1]的最大值, 肯定是在a[2][0]和a[2][1]里面产生, 这样就只和第三层记录的最大值有关, 和之前的没有关系了 可以利用这种方法一层一层的更新, 最后找一次最大值就可以了
    所以这个题的
    状态 : dp[i][j] 它 表示的在i+1层的第j + 1个位置能得到的最大值,
    状态转移 : dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]) + a[i][j]
    初始化 : dp[0][0] = a[0][0] 其他的dp可以不管

动态规划的递推和递归

递推:
利用循环的方式一层一层的推下去 最后得到解得方法 比如这个题, 状态和状态转移都已经知道了 那么代码就很简单了

#include 
#include  //为了使用max
using namespace std;

const int N = 105;

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

int main() {
    int n; scanf("%d", &n);//录入数据
    for(int i = 0; i < n; ++i)
        for(int j = 0; j <= i; ++j)
            scanf("%d", &a[i][j]);

    dp[0][0] = a[0][0];//初始化
    for(int i = 1; i < n; ++i) {
        dp[i][0] = dp[i-1][0] + a[i][0];  //边界问题特殊考虑
        for(int j = 1; j < i; ++j) {
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-1]) + a[i][j];//状态转移
        }
        dp[i][i] = dp[i-1][i-1] + a[i][i];  //边界问题特殊考虑
    }

    int ans = 0;
    for(int i = 0; i < n; ++i) ans = max(ans, dp[n-1][i]);
    printf("%d\n", ans);
}

递归:
利用系统的栈的方式得到答案, 会用到记忆化, 因为如果不记忆化同一个东西可能会算很多次
代码一般是这样的:

#include 
#include  //为了使用max
#include  //为了使用memeset
using namespace std;

const int N = 105;

int dp[N][N], a[N][N];//

int dfs(int i, int j) {
    if(i == 0 && j == 0) return a[0][0];
    if(dp[i][j] != -1) return dp[i][j];//记忆化 如果已经有值了直接返回
    if(j == 0) return dp[i][j] = dfs(i-1, j) + a[i][j];
    else if(j == i) return dp[i][j] = dfs(i-1, j-1) + a[i][j];
    else return dp[i][j] = max(dfs(i-1, j), dfs(i-1, j-1)) + a[i][j];
}

int main() {
    int n; scanf("%d", &n);//录入数据
    for(int i = 0; i < n; ++i)
        for(int j = 0; j <= i; ++j)
            scanf("%d", &a[i][j]);

    memset(dp, -1, sizeof dp);

    int ans = 0;
    for(int i = 0; i < n; ++i) ans = max(ans, dfs(n-1, i));
    printf("%d\n", ans);
}

当然这个三角形你还可以倒着看 做法很多 (其实还可以用图论的方法做)
用递推的方法都会比递归省时间, 但一般题目不会卡这个, 递归的方法对于某些题来说看上去更容易懂
3. 例题
题目链接 : http://acm.hust.edu.cn/vjudge/problem/visitOriginUrl.action?id=120008
ACM_普通DP_第4张图片
ACM_普通DP_第5张图片
ACM_普通DP_第6张图片
题目大意: 中文 =_= …..
做题思路: 很容易就可以想到状态 dp[l][r][i] 表示走到了第i个位置, 左手指在l, 右手指在r的最小值, 但是内存爆了, 于是可以用到滚动数组, 因为注意到每次更新的时候只需要i个位置左边哪个位置的那些dp值 前面的不需要了, 所以 只需要开dp[4][4][2]的内存就够了
状态转移: 到了第i个位置的时候必定有个手指会到这个位置所指示的键上, 所以更新每个状态的左手指到这个位置和右手指到这个位置就可以了
初始化:题目说了最开始左手指在左右键上所以dp[2][3][0] = 0其他的dp 等与正无穷就OK了
注意滚动数组的时候记得每次重新把他们变成正无穷
代码如下:

//
//  Created by fkjs on 2015-11-12
//  Copyright (c) 2015 fkjs. All rights reserved.
//
//#pragma comment(linker, "/STACK:1024000000,1024000000")
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

typedef long long int ll;
const int INF = 0x3f3f3f3f;

int w[4][4] = {0, 1, 2, 2, 1, 0, 1, 1, 2, 1, 0, 2, 2, 1, 2, 0};
int dp[4][4][2];

int main(void) {
#ifdef LOCAL
    //freopen("C:\\Users\\fkjslee\\Desktop\\in.txt", "r", stdin);
    //freopen("C:\\Users\\fkjslee\\Desktop\\out.txt", "w", stdout);
#endif
    //ios_base::sync_with_stdio(0);

    string s;
    while(cin >> s) {
        memset(dp, INF, sizeof dp);
        dp[2][3][0] = 0;
        int op;
        int sign = 0;
        int sz = s.size();
        for(int i = 0; i < sz; i++) {
            op = s[i] - '0';
            sign = !sign;
            for(int l = 0; l < 4; l++) {
                for(int r = 0; r < 4; r++) {
                    if(l == op || r == op) dp[l][r][sign] = min(dp[l][r][sign], dp[l][r][!sign]);
                    else {
                        dp[op][r][sign] = min(dp[op][r][sign], dp[l][r][!sign] + w[l][op]);
                        dp[l][op][sign] = min(dp[l][op][sign], dp[l][r][!sign] + w[r][op]);
                    }
                }
            }
            for(int l = 0; l < 4; l++)
                for(int r = 0; r < 4; r++)
                    dp[l][r][!sign] = INF;
//                for(int l = 0; l < 4; l++)
//                    for(int r = 0; r < 4; r++)
//                        printf("dp[%d][%d][%d] = %d\n", l, r, sign, dp[l][r][sign]);
//                puts("");
        }
        int ans = INF;
        for(int i = 0; i < 4; i++) ans = min(ans, dp[op][i][sign]);
        for(int i = 0; i < 4; i++) ans = min(ans, dp[i][op][sign]);
        cout << ans << endl;
    }
    return EXIT_SUCCESS;
}

你可能感兴趣的:(动态规划)