动态规划

dp

 

 数字三角形

例题1:

AcWing 1015. 摘花生 - AcWing

题意:

从(1,1)到(r,c)摘花生,不可走回头路,最多可以摘多少

思路:

f[i, j]:(1,1)到(i,j)的最大摘花生数

核心代码:

        for(int i = 1; i <= r; i++)
        {
            for(int j = 1; j <= c; j++)
            {
                f[i][j] = max(f[i - 1][j], f[i][j - 1]) + a[i][j];
            }
        }

        cout << f[r][c] <<'\n';

例题2

1027. 方格取数 - AcWing题库

题意:

从(1,1)到(n,n)在每一个方格中取数,方格中数被取后数变成0,可走两次,问可取最大数为几

思路:

用两次dp无法确定是否是最优解

用f[i1][j1][i2][j2]表示从(1,1)开始同时走分别走到(i1,j1)(i2,j2)的最大取数

i1+j1=i2+j2时才是同时走的

设k = i1+j1

用三维dp,f[k][i1][i2]求解(横纵坐标和为k时,纵坐标走的i1,i2的时候,最大取数)

核心代码:

    for (int k = 2; k <= n + n; k ++ )
        for (int i1 = 1; i1 <= n; i1 ++ )
            for (int i2 = 1; i2 <= n; i2 ++ )
            {
                int j1 = k - i1, j2 = k - i2;
                if (j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n)
                {
                    int t = w[i1][j1];
                    if (i1 != i2) t += w[i2][j2];
                    int &x = f[k][i1][i2];
                    x = max(x, f[k - 1][i1 - 1][i2 - 1] + t);
                    x = max(x, f[k - 1][i1 - 1][i2] + t);
                    x = max(x, f[k - 1][i1][i2 - 1] + t);
                    x = max(x, f[k - 1][i1][i2] + t);
                }
            }

    printf("%d\n", f[n + n][n][n]);

最长上升子序列(LIS)

例题1

895. 最长上升子序列 - AcWing题库

f[i] :所有以a[i]结尾的单调上升子序列长度最大值

核心代码:

    for(int i = 1; i <= n; i++)
    {
        f[i] = 1;
        for(int j = 1; j < i; j++)
        {
            if(a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
        }
    }

    int ans = 0;

    for(int i = 1; i <= n; i++)
    {
        ans = max(ans, f[i]);
    }

    printf("%d", ans);

例题2

1017. 怪盗基德的滑翔翼 - AcWing题库 

题意:

输入一个长度为n的序列,可以从1~n任意位置做起点,可以向左或向右走,求最长子序列长度(向左走求最长上升子序列长度 eg:12345,向右走求最长下降子序列长度 eg: 54321)

思路:

两个数组分别存向左/向右的长度,求Max

核心代码:

        for(int i = 1; i <= n; i++)
        {
            rf[i] = 1;
            for(int j = 1; j < i; j++)
            {
                if(a[i] < a[j]) rf[i] = max(rf[i] , rf[j] + 1);
            }
        }
        for(int i = n; i >= 1; i--)
        {
            lf[i] = 1;
            for(int j = n; j > i; j--)
            {
                if(a[i] < a[j]) lf[i] = max(lf[i] , lf[j] + 1);
            }
        }

        for(int i = 1; i <= n; i++) ans = max(rf[i], max(ans, lf[i]));
        cout << ans << '\n';

例题3

AcWing 1014. 登山 - AcWing

题意:

给定景点编号,进行观光,要求:按照顺序给定顺序递增浏览,相邻两个景点不能相同,一旦开始下降了就不能上升了

思路:

求出如上题的rf[i], lf[i],将第i位上的rf[i] 与 lf[i]相加,求Max

核心代码:

    for(int i = 1; i <= n; i++)
    {
        f[i] = 1;

        for(int j = 1; j < i; j++)
        {
            if(a[j] < a[i]) f[i] = max(f[i], f[j] + 1);
        }
    }

    for(int i = n; i >= 1; i--)
    {
        g[i] = 1;

        for(int j = n; j > i; j--)
        {
            if(a[j] < a[i]) g[i] = max(g[i], g[j] + 1);
        }
    }

    for(int i = 1; i <= n; i++) ans = max(ans, f[i] + g[i] - 1);

例题4

AcWing 1012. 友好城市 - AcWing

题意:

河对岸各有一些城市,给两岸的城市建桥,问最多可以建多少桥?

 

 思路:

排序后求LIS

核心代码:

 for(int i = 1; i <= n; i++) cin >> town[i].a >> town[i].b;

    sort(town+1, town+n+1, cmp);

    for(int i = 1; i <= n; i++)
    {
        f[i] = 1;
        for(int j = 1; j < i; j++)
        {
            if(town[j].b < town[i].b) f[i] = max(f[i], f[j] + 1);
        }
    }

    for(int i = 1; i <= n; i++) ans = max(f[i], ans);

    cout << ans;

例题5

1010. 拦截导弹 - AcWing题库

题意:

有一系列导弹,现在要用导弹系统拦截,每套导弹系统拦截导弹高度只能不断向下

第一问:每套导弹拦截系统拦截导弹高度为下降子序列,求最长的就好了

第二问:求导弹拦截系统的个数可以转化为求最长上升子序列长度

思路:

第一问:参考例题1,上升变为下降

第二问:用不同序列来存贮导弹高度,满足以下条件

1.如果现有子序列结尾小于当前数,创建新子序列

2.当前的数放到大于等于他的最小的子序列后面

序列数为答案

核心代码

    for(int i = 0; i < n; i++)
    {
        f[i] = 1;

        for(int j = 0; j < i; j++)
        {
            if(a[j] >= a[i])   f[i] = max(f[i], f[j] + 1);
        }

        res = max(res, f[i]);
    }

    cout << res << '\n';

    int cnt = 0;

    for(int i = 0; i < n; i++)
    {
        int k = 0;

        while(k < cnt && g[k] < a[i]) k++;

        g[k] = a[i];//g[k]存贮每一序列的最后一个数

        if(k >= cnt) cnt++;//开启新序列
    }

    cout << cnt << '\n';

例题6

187. 导弹防御系统 - AcWing题库

题意:

题目给定一个长度为 n 的数组 w[n] ,要求我们用最少的上升子序列和下降子序列完全覆盖该数组

求该方案的上升子序列和下降子序列的总个数

思路:

最核心的思想在于能否将一个元素加入到序列中,只与这个序列目前的最后一个元素有关

这道题就用了这个关键的思想。

用up[k]和down[k]记录第k套上升(下降)系统目前所拦截的最后一个导弹

dfs(u,v,t)意味着已有u个上升,v个下降,正在处理第t个数

是例题5的扩展

代码:

#include<iostream>
using namespace std;
const int N = 60;
int n;
int h[N];
int ans;
int up[N], down[N];
void dfs(int u, int su, int sd)//当前导弹编号 ,上升序列个数  ,下降序列个数 
{
    if(su+sd >= ans)//无法出现更优解减枝
        return;
    if(u == n){
        if(su + sd < ans)ans = su + sd;//出现更优解,更新
        return;
    }

    int k = 0;
    //上升子序列
    while(k < su && up[k] >= h[u]) 
        k++;
    
    if(k < su)//放入一个已有子序列中
    {
        int tem = up[k];
        up[k] = h[u];
        dfs(u+1, su, sd);
        up[k] = tem;
    }else{//开一个新序列
        up[k] = h[u];
        dfs(u+1, su+1, sd);
    }

    //下降子序列
     k = 0;
     while(k < sd && down[k] <= h[u])
        k++;
    
    if(k < sd)
    {
        int tem = down[k];
        down[k] = h[u];
        dfs(u+1, su, sd);
        down[k] = tem;
    }else{
        down[k] = h[u];
        dfs(u+1, su, sd+1);
    }

}
int main()
{
    while(cin >> n, n) 
    {
        for(int i = 0; i < n; i++)
            scanf("%d", &h[i]);
        ans = n;
        dfs(0 ,0 ,0);
        cout << ans << '\n';
    }

    return 0; 
        
}

例题7

272. 最长公共上升子序列 - AcWing题库

题面:

求最长公共上升子序列长度

思路:

f[i,j]分为两种:

1.所有不包含a[i]的公共上升子序列f[i,j] = 

2.所有包含a[i]的公共上升子序列,根据a[i]与b中哪一个数相同来计算

有f[i,k]+1 

  

代码:

    for (int i = 1; i <= n; ++ i)
    {
        for (int j = 1; j <= n; ++ j)
        {
            f[i][j] = f[i - 1][j];//先考虑右边情况
            if (a[i] == b[j])//考虑左边情况
            {
                for (int k = 0; k < j; ++ k)
                {
                    if (b[j] > b[k])
                    {
                        f[i][j] = max(f[i][j], f[i - 1][k] + 1);
                    }
                }
            }
        }
    }
  
    int res = 0;
    for (int i = 0; i <= n; ++ i) res = max(res, f[n][i]);
    cout << res << '\n';

代码优化:

    for (int i = 1; i <= n; ++ i)
    {
        int maxv = 1;
        for (int j = 1; j <= n; ++ j)
        {
            f[i][j] = f[i - 1][j];
            if (b[j] == a[i]) f[i][j] = max(f[i][j], maxv);
            if (b[j] < a[i]) maxv = max(maxv, f[i][j] + 1);
        }
    }

背包问题

01背包:

每个物品个数为1,对于他只有选与不选两种状态

 

 

 f[i, j]表示只从前i个物品里面选,总体积小于等于j的方案数

二维代码:

#include <iostream>
#include<cmath>
using namespace std;
int f[1010][1010];
int v[1010];
int w[1010];
int main()
{
    int n, m;
    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]) //这一步目为了取上一次选与不选中的最大值
            {
                //max()内的f[i][j]相当于f[i-1][j]
                f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
            }
        }
    }

    printf("%d", f[n][m]);
    return 0;
}

代码优化:

通过观察状态转移方程:f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
我们可以发现 f 的第一维只有 i 与 i-1, 没有 i-2 ,i-3 .....可以考虑使用滚动数组
我们尝试去掉 f 的第一维


优化思路
    for (int i = 1; i <= n; i++)
    {
        for (int j = v[i]; j <= m; j++)
        {
            //f[j] = [j];一个等式省略不写

            //if (j >= v[i]) //可以放在for循环条件内
            //{
               // j-v[i] < j 所以 j 遍历的时候,这一层的 j-v[i]必定已经被遍历过,此时的 f[j - v[i]] 表示的是 f[i][j - v[i]]]
          //所以对j的枚举要从大到小
          f[i] = max(f[j], f[j - v[i]] + w[i]);
                //f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
            //}
        }
    }

优化后代码:

for (int i = 1; i <= n; i++)
{
    for (int j = m; j >= v[i]; j--)
    {             
       f[j] = max(f[j], f[j]-v[i]] + w[i]);
    }
}

二维01背包

题目:

8. 二维费用的背包问题 - AcWing题库

代码:

#include <iostream>
#include <algorithm>

using namespace std;
const int N = 1010, M = 110;

int f[N][N];
int v[N];
int g[N];
int w[N];

int main()
{
    int n ,V, m;
    
    cin >> n >> V >> m;

    for(int i = 1; i <= n; i++)
    {
        cin >> v[i] >> g[i] >> w[i];
    }

    for(int i = 1; i <= n; i++)//个数
    {
        for(int j = m; j >= g[i]; j--)//重量
        {
            for(int h = V; h >= v[i]; h--)//体积
            {
                    f[j][h] = max(f[j][h], f[j-g[i]][h-v[i]]+w[i]);
            }
        }
    }

    cout << f[m][V];

    return 0;
}

 

 

完全背包

每个物品有 k 个

f[i, j] 表示只考虑前i个物品(物品可选0~k个)且体积不大于 j 的选法

对于 f[i, j] 我们选取 f[i-1][] 层时,我们对选 0~k 个物品选取其中最大值

 

朴素写法

 

for (int i = 1; i <= n; i++)
{
    for (int j = 0; j <= m; j++)
    {
        for (int k = 0; k * v[i] <= j; k++)
            f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    }
}

优化思路

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)
发现
f[i, j] = max(f[i-1,j], f[i , j-v]+W);
由此发现可以去掉循环中的 k

优化后代码:

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] >= 0)
            f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
    }
}

01背包与完全背包区别

多重背包

对于每个物品个数有 Si 限制

 

朴素版本与完全背包问题类似

    for(int i = 1; i <= n; i ++){//枚举背包
        for(int j = 1; j <= m; j ++){//枚举体积
            for(int k = 0; k <= s[i]; k ++){
                if(j >=  k * v[i]){
                    f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
                }
            }
        }
    }

 二进制优化:

对于任意一个十进制,我们都可以用几个二进制数来表示,所以对于某一个数。我们可以优化为一些二进制数的选与不选(01背包问题)

代码:

//将每种物品根据物件个数进行打包
int cnt = 0;
for(int i = 1; i <= n; i ++){
    int a, b, s;
    cin >> a >> b >> s;//体积、价值和数量
    int k = 1;//用来表示2的几次方
    while(k <= s){
        cnt ++;
        v[cnt] = k * a;
        w[cnt] = k * b;
        s -= k;
        k *= 2;
    }
    if(s > 0){
        cnt ++;
        v[cnt] = s * a;
        w[cnt] = s * b;
    }
}
//多重背包转化为01背包问题
for(int i = 1; i <= cnt; i ++){
    for(int j = m; j >= v[i]; j --){
        f[j] = max(f[j], f[j - v[i]] + w[i]);
    }
}

cout << f[m] << '\n';

分组背包:

 

对于只选前 i 组物品,且体积不超过 j 的方案数

 v[i, k] 第 i 组物品的第 k 个物品

int n, m; //输入物品组数与背包容量

scanf("%d%d", &n, &m);

for (int i = 1; i <= n; i++)
{
    int s;
    scanf("%d", &s); //输入第i个物品组的物品数量

    for (int j = 1; j <= s; j++)
        scanf("%d%d", &v[j], &w[j]); //输入每个物品的体积和价值

    //接下来是处理过程

    for (int j = m + 1; j >= 1; j--) //枚举所有体积
        for (int k = 1; k <= s; k++) //枚举所有选择
            if (j >= v[k])           //必须要满足,否则下面的下标减出来是负数
                f[j] = max(f[j] /*不选*/, f[j - v[k]] + w[k] /**/);
}
printf("%d", f[m]);

 

状态机:

找到入口出口,状态转移情况

例题1:

1058. 股票买卖 V - AcWing题库

思路:

代码:

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, INF = 0x3f3f3f3f;

int n;
int w[N];
int f[N][3];

int main()
{
    scanf("%d", &n);

    for (int i = 1; i <= n; i ++ ) 
        scanf("%d", &w[i]);

    f[0][0] = f[0][1] = -INF, f[0][2] = 0;
    
    for (int i = 1; i <= n; i ++ )
    {
        f[i][0] = max(f[i - 1][0], f[i - 1][2] - w[i]);
        f[i][1] = f[i - 1][0] + w[i];
        f[i][2] = max(f[i - 1][2], f[i - 1][1]);
    }

    printf("%d\n", max(f[n][1], f[n][2]));

    return 0;
}

 

posted on 2023-03-11 11:34  玛卡巴卡要ac  阅读(29)  评论(0)    收藏  举报

导航