动态规划1——通用切题思路

y总的讲解视频 https://www.bilibili.com/medialist/play/watchlater/BV1X741127ZM

关键词

  • 从集合角度来分析
  • 有限集中的最优化问题(最大值/最小值/个数/存在与否)
  • 自然的思路是指数级的,需要优化
  • 先化零为整,将一些有共同特征的元素化为一个子集,用 特定的状态 来表示,考察集合是什么,表示集合的元素存的内容是什么(一般情况就是题目问的对象)
  • 再化整为零,即 状态计算, 寻找最后一个不同点,将集合划分为若干个子集,保证不遗漏,不重复(视情况而定)
  • 始终抓住 集合的定义
0-1背包问题

原题链接 https://www.acwing.com/problem/content/2/

//朴素写法  时间O(n^2)  空间O(n^2)
#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N][N];  //f[i][j]表示所有只考虑前i个物品且总体积不超过j的方案中价值的最大值

//递推公式
//f[i][j] = max( f[i-1][j], (f[i - 1][j - v[i]] + w[i]) )
//分成两个子集:1.包含最后一个物品; 2.不包含最后一个物品

int main() {
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for(int i = 1; i <= n; i ++ ) {
        for(int j = 0; j <= m; j ++ ) {
            f[i][j] = f[i - 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); //在背包容量大于v[i]的前提下,才能递推
        }
    }
    
    cout << f[n][m] << endl;  //最大价值是考虑所有n个物品且总体积不超过m的方案中的最大价值
    return 0;
}

//优化空间复杂度写法 时间O(n^2)  空间O(n)
//滚动数组,去掉第一维,因为每次计算当前层时,只需要用到上一层的结果
//dp问题的所有优化,都是对代码做等价变形
#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];

int main() {
    cin >> n >> m;
    
    for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for(int i = 1; i <= n; i ++ ) {
        for(int j = m; j >= v[i]; j -- ) { //这儿j从大到小循环,保证了下面求递推时,f[j - v[i]]是上一层循环计算出来的值,即f[i - 1][j - v[i]],在这层还未被更新
            if(j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    
    cout << f[m] << endl;
    return 0;
}
完全背包问题

原题链接 https://www.acwing.com/problem/content/3/

/*
1. 01背包:  f[i][j] = max(f[i - 1][j], f[i - 1][j - vi] + wi)
2. 完全背包:f[i][j] = max(f[i - 1][j], f[i][j - vi] + wi);
*/
//朴素写法是需要一个二维矩阵来表示状态的,但是通过观察分析模拟过程,我们发现空间是可以复用的,很早之前的状态后续不再需要
//我们只需要保留后续计算还需要的值,可以用一个“滚动数组”来替代二维数组
//背包问题如果用二维矩阵记录状态,第二层的循环顺序不受限制,如果优化成滚动数组,则01背包从大到小循环,完全背包从小到大
//具体来讲,考察当前计算所需的那个状态需不需要更新

#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int f[N]; // 在考虑边界和状态转移时始终抓住这点定义!!!

int main() {
    cin >> n >> m;
    
    for(int i  = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
    
    for(int i = 1; i <= n; i ++ ) {
        for(int j = v[i]; j <= m; j ++ ) { //与0-1背包唯一一句区别
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
	
    cout << f[m] << endl;
    return 0;
}
石子合并(区间dp)

原题链接:https://www.acwing.com/problem/content/284/

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 310;
int n;
int s[N];
int f[N][N]; //f[i][j]表示将[i, j]合并成一堆的方案的集合中的最小代价

int main() {
    cin >> n;
    for(int i = 1; i <= n; i ++ ) cin >> s[i];
    
    for(int i = 1; i <= n; i ++ ) s[i] += s[i - 1];  //求前缀和
    
    for(int len = 2; len <= n; len ++ ) //枚举区间长度
        for(int i = 1; i + len - 1 <= n; i ++ ) { //枚举起点
            int l = i, r = i + len - 1;
            f[l][r] = 2e9;
            for(int k = l; k < r; k ++ )//枚举分界点
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
        
    cout << f[1][n] << endl;
    return 0;
}
最长公共子序列

原题链接:https://www.acwing.com/problem/content/899/

//求数量要求不重不漏,求最值得时候可以重复
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];  //f[i][j]表示所有 a[1 - i]与  b[1 - j]的公共子序列的集合当中的最大长度

int main() {
    cin >> n >> m;
    scanf("%s%s", a + 1, b + 1);
    
    for(int i = 1; i <= n; i ++ ) {
        for(int j = 1; j <= m; j ++ ) {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);  //这儿的状态表示和子集并不是完美对应的,状态表示的范围大于实际需要表示的子集,但是因为是求最大值,所以不会影响最终结果
            if(a[i] == b[j]) f[i][j] = max(f[i - 1][j - 1] + 1, f[i][j]);
        }
    }
    
    cout << f[n][m] << endl;
    return 0;
}
posted @ 2021-04-07 15:48  呼_呼  阅读(80)  评论(0编辑  收藏  举报