动态规划——数字三角形模型(线性DP)

线性DP——数字三角形模型


动规基本介绍

很重要的划分依据:最后(最后一步的状态由前一步状态得出)

集合的划分原则:

  • 不重复
  • 不漏(必定有)

1.数字三角形

【题目链接】898. 数字三角形 - AcWing题库

在这里插入图片描述

正推思路:

在这里插入图片描述

时间复杂度:O(n*n)

【代码实现】

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

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;
int a[N][N];
int f[N][N];

int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i ++ )
        for(int j = 1; j <= i; j ++)
            cin >> a[i][j];
            
    //因为有负数,所以应该将两边也设为-INF
    for(int i = 0; i <= n; i ++)
        for(int j = 0; j <= i + 1; j ++)
            f[i][j] = -INF;
    
    f[1][1] = a[1][1];
    for(int i = 2; i <= n; i ++)
        for(int j = 1; j <= i; j ++)
            f[i][j] = max(f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j]);
    
    //求走到最后一层的所有方案的最大值
    int res = -INF;
    for(int j = 1; j <= n; j ++) res = max(res, f[n][j]);
    
    cout << res;
    
    return 0;
}

【逆推求解】

从后往前推,即答案为f[1][1],表示由(1,1)点到最后一层的值的最大和。

  • 不难发现,最后一层的点到最后一层的最大距离即为自己对应的值a[n - 1][y],这个就是问题的边界。
  • 从后往前推,观察发现当前点的状态只与正下方和右下方的状态有关,因此得出递推式(状态转移方程):f[i][j] = a[i][j] + max(f[i+1][j],f[i + 1][j + 1])

【代码实现】

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

using namespace std;

const int N = 500 + 10;
int a[N][N];
int f[N][N];
int n;
int ans = -1e9;


int main()
{
    cin >> n;
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= i; j ++)
            cin >> a[i][j];
    
    //最后一层
    for(int j = 1; j <= n; j ++) f[n][j] = a[n][j];        
    
    // 从后往前推
    for(int i = n - 1; i >= 1; i --)
        for(int j = 1; j <= i; j ++)
        {
            f[i][j] = a[i][j] + max(f[i + 1][j], f[i + 1][j + 1]);
        }
    cout << f[1][1];
    return 0;
}

动规:记忆化搜索:

  • 求解每一个点的值,先判断该点的值是否曾经求解过,如果曾经求解过,直接拿过来使用;如果没求解过,递归求解,并存储该解!
  • 将计算过的值存储到一个数组中
  • 如何判断是否求解过呢?——做标记判断

【代码实现】

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

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;
int a[N][N];
int f[N][N];
int n;
//动规,记忆化搜索:先将d数组初始化为-1,方便判断有没有求解过
int fun(int x, int y)
{
    if(x == n) return f[x][y] = a[x][y];// 最后一层的解就是自己
    
    if(f[x][y] != -1) return f[x][y];// 曾经求解过
    
     // 求解(x,y)点走到底层经过的数字和的最大值,并存储
    f[x][y] = a[x][y] + max(fun(x + 1, y + 1), fun(x + 1, y));
    
    return f[x][y];
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )
        for(int j = 1; j <= i; j ++)
            cin >> a[i][j];
            
    memset(f, -1, sizeof f);
    
    cout << fun(1, 1);
    
    return 0;
}

2.摘花生

【题目链接】AcWing 1015. 摘花生 - AcWing

Hello Kitty想摘点花生送给她喜欢的米老鼠。

她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。

地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。

Hello Kitty只能向东或向南走,不能向西或向北走。

问Hello Kitty最多能够摘到多少颗花生。

1.gif

输入格式第一行是一个整数T,代表一共有多少组数据。

接下来是T组数据。

每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C。

每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有C个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目M。

输出格式
对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数。

数据范围
1≤T≤100,
1≤R,C≤100,
0≤M≤1000
输入样例:
2
2 2
1 1
3 4
2 3
2 3 4
1 6 5
输出样例:
8
16

题意:从起点(1, 1)走到终点(n, m)的最大重量是多少?

思路:

在这里插入图片描述

时间复杂度:

【代码实现】

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

using namespace std;

const int N = 110;
int w[N][N];
int f[N][N];


int main()
{
    int T;
    cin >> T;
    while(T --)
    {
        int n, m;
        cin >> n >> m;
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                cin >> w[i][j];
        
        //f[1][1] = w[1][1]; 不要也行,全局变量f[0][j] = f[i][0] = 0
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                f[i][j] = w[i][j] + max(f[i -1][j], f[i][j -1]);
        
        cout << f[n][m] << endl;        
    }
    
    return 0;
}

记忆化搜索:

【代码实现】

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

using namespace std;

const int N = 110, INF = 0x3f3f3f3f;
int w[N][N];
int f[N][N];
 int n, m;
 
 int fun(int x, int y)
 {
     if(f[x][y] != -1) return f[x][y];
     if(x < 1 || x > n || y < 1 || y > m) return f[x][y] = -INF;
     if(x == 1 && y == 1) return f[x][y] = w[x][y];
     
     int t = 0;
     t = max(t, fun(x -1, y) + w[x][y]);
     t = max(t, fun(x, y - 1) + w[x][y]);
     
     return f[x][y] = t;
 }


int main()
{
    int T;
    cin >> T;
    while(T --)
    {
        memset(w, 0, sizeof w);
        memset(f, -1, sizeof f);
        cin >> n >> m;
       
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                cin >> w[i][j];
                
        cout << fun(n ,m) << endl;

    }
    
    return 0;
}

一维优化:

【代码实现】

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

using namespace std;

const int N = 110;
int w[N][N];
int f[N];


int main()
{
    int T;
    cin >> T;
    while(T --)
    {
        int n, m;
        cin >> n >> m;
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                cin >> w[i][j];
        
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                f[j] = w[i][j] + max(f[j], f[j -1]);
        
        cout << f[m] << endl;    
        
        // 由于多组样例,而二维数组解法由于f[0][...]和f[...][0]都为0,所以没有问题。对于一维数组,上一样例的f数组需要清零,否则影响结果
        memset(f, 0, sizeof f);
    }
    
    return 0;
}

3.最低通行费

【题目链接】1018. 最低通行费 - AcWing题库

在这里插入图片描述

思路:

题目中2N-1可以推出不能走回头路只能往下或者往右走,可以转化为摘花生的问题。

在这里插入图片描述

这题不同于摘花生的地方在于,他的属性是最小值,因此需要在代码上作出一点点改变

例如,需要先把所有状态初始化正无穷初始化状态的起点dp求最小值必须要的步骤

以及,状态转移时的越界判断

【代码实现】

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

using namespace std;

const int N = 110;
int w[N][N];
int f[N][N];
int n;

int main()
{
    cin >> n;
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            cin >> w[i][j];
            
    memset(f, 0x3f, sizeof f);
    f[1][1] = w[1][1];
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
        {
            f[i][j] = min(f[i][j], f[i -1][j] + w[i][j]);
            f[i][j] = min(f[i][j], f[i][j -1] + w[i][j]);
        }
    
    cout << f[n][n] << endl;    
            
    return 0;
}

记忆化搜索:

【代码实现】

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

using namespace std;

const int N = 110, INF = 0x3f3f3f3f;
int w[N][N];
int f[N][N];
int n;

int fun(int x, int y)
{
    if(f[x][y] != -1) return f[x][y];
    if(x < 1 ||  y < 1) return f[x][y] = INF;
    if(x == 1 && y == 1) return f[x][x] = w[x][y];
    
    int t = INF;
    t = min(t, fun(x - 1, y));
    t = min(t, fun(x, y - 1));
    
    return f[x][y] = t + w[x][y];
    
}

int main()
{
    cin >> n;
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            cin >> w[i][j];
            
    memset(f, -1, sizeof f);
    
    cout << fun(n, n) << endl;
            
    return 0;
}

4. Weak Takahashi

【题目链接】D - Weak Takahashi (atcoder.jp)

题意:从起点开始,可以向右向下走,问最多可以经过'.'个个数。

思路:通过递推,由当前已知点推出未知点

在这里插入图片描述

【代码实现】

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <cstring>
#include <map>
#include <queue>
#include <deque>
#include <vector>
#include <string.h>
#include <unordered_set>
#include <unordered_map>

#define x first
#define y second

using namespace std;

typedef long long LL;
typedef pair<int, int> PII;
typedef pair<int, PII> PIII;

const int N = 110, M = 2e5 + 10;

int dp[N][N];//到(i,j)'.'的个数为多少:下一个位置是'.'上一个位置的'.'个数加一,'#'跳过即可
char g[N][N];
int n, m;


int main() 
{
    
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) cin >> g[i] + 1;//下标从1开始

    dp[1][1] = 1;
    
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
        if(dp[i][j])
        {
            if(i != n && g[i + 1][j] == '.')
                dp[i + 1][j] = dp[i][j] + 1;
            if(j != m && g[i][j + 1] == '.')
                dp[i][j + 1] = dp[i][j] + 1;
        }

    int res = 0;    
    for(int i = 1; i <= n; i ++)
    {     
        for(int j = 1; j <= m; j ++)
        {
            // cout << dp[i][j] <<  ' ';
            res = max(res, dp[i][j]);
        }
        // cout << endl;
    }    
    cout << res << endl;    

    return 0;  
        
}

5.方格取数

【题目链接】

思路:

题目要求我们从起点先后出发两次

但我们可以规定两次是同时出发的,因为这两种方案的所有路线都是一一对应的
在这里插入图片描述

状态表示:

考虑两条路径同时走的情况,用f[i1][j1][i2][j2]表示第一条路径走到(i1,j1), 第二条路径走到(i2,j2)位置的取数最大值,那么很显然,结果就是f[n][n][n][n] 其中i,j分别是行号和列号

这个状态可以进一步化简,用k来表示某条路径的行号和列号之和,比如一条路径为i1+j1,这样状态表示就压缩到了三维f[k][i1][i2],其中i1表示第一条路径的行号,i2表示第二条路径的行号

可以依次枚举k,i1,i2,这样就可以得到每条路径的行号和列号

状态转移:

有了状态表示,那么状态转移就很好想了。因为这两条路径都只可以往下走或者往右走(划分依据同样也是最后),所以当前状态的上一个状态不外乎四种情况

  • 第一条路径往下走,第二条路径往下走, 即f[k−1][i1−1][i2−1]
  • 第一条路径往下走,第二条路径往右走,即f[k−1][i1−1][i2]
  • 第一条路径往右走,第二条路径往右走,即f[k−1][i1][i2]
  • 第一条路径往右走,第二条路径往下走,即f[k−1][i1][i2−1]

此外需要注意的一点,k并不是指两条路径的路径和,它只表示了上一种状态
因为ij最小时,k = 2ij最大时,k = n + n,所以k的总的状态范围为2n + n

记上一个状态走到当前状态取数为t,如果这两条路径在当前这个点重合(重复走)时,即i1=i2,j1=j2时,那么取数t=w[i1][j1], 否则为t=w[i1][j1]+w[i2][j2]

t分别加上上一种状态的四种情况,取个max就是当前状态的最大值

【代码实现】

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

using namespace std;

const int N = 20;
int w[N][N];
int f[N * 2][N][N];
int n;


int main()
{
    cin >> n;
    int a, b, c;
    while(cin >> a >> b >> c, a || b || c) w[a][b] = c;
    
    for(int k = 2; k <= 2 * 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);
                }
            }
    cout << f[n + n][n][n] << endl;        

    return 0;
}

记忆化搜索:

【代码实现】

#include <iostream>
#include <cstring>

using namespace std;

const int N = 15, M = 2 * N, INF = 0x3f3f3f3f;

int n;
int a, b, c;
int w[N][N];
int f[M][N][N];

int fun(int k, int i1, int i2)
{
    if (f[k][i1][i2] >= 0) return f[k][i1][i2];
    //起点判断
    if (k == 2 && i1 == 1 && i2 == 1) return f[k][i1][i2] = w[1][1];
    //越界判断
    if (i1 <= 0 || i1 >= k || i2 <= 0 || i2 >= k) return f[k][i1][i2] = -INF

    //重复格子判断
    int t = w[i1][k - i1];
    if (i1 != i2) t += w[i2][k - i2];

    //状态转移
    int &x = f[k][i1][i2];
    x = max(x, fun(k - 1, i1, i2));
    x = max(x, fun(k - 1, i1 - 1, i2));
    x = max(x, fun(k - 1, i1, i2 - 1));
    x = max(x, fun(k - 1, i1 - 1, i2 - 1));

    return f[k][i1][i2] = x + t;
}
int main()
{
    memset(f, -1, sizeof f);
    cin >> n;
    while (cin >> a >> b >> c, a || b || c) w[a][b] += c;
    cout << fun(n + n, n, n) << endl;
    return 0;
}

6.传纸条

【题目链接】275. 传纸条 - AcWing题库

思路:

对于一个从 (n,m)出发到(1,1)的路线,且只能向上或向右走,考虑将其方向调转,则必定对应一条从(1,1) 出发到(n,m)的路线,且只能向下或向右走。

这样就很靠近 方格取数 模型了

与方格取数不同的是, 一个方格只能经过一次
处理方案: 当走到同一个方格时,令f[k][i1][i2]=−INF, 则后继状态不会使用这个状态, 就保证了不会走过同一个方格

【代码实现】

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

using namespace std;

const int N = 60, INF = 0x3f3f3f3f;
int w[N][N];
int f[N * 2][N][N];
int n, m;


int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            cin >> w[i][j];
    
    for(int k = 2; k <= n + m; k ++)
        for(int i1 = 1; i1 < k && i1 <= n; i1 ++)
            for(int i2 = 1; i2 < k && i2 <= n; i2 ++)
            {
                int j1 = k - i1, j2 = k - i2;
                if(j1 >= 1 && j1 <= m && j2 >= 1 && j2 <= m)
                {
                    if (i1 == i2 && k != 1 && k != m + n) { f[k][i1][i2] = -INF; continue; }    // 起点和终点必须都走
                    int t = w[i1][j1] + 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);
                }
            }
    cout << f[n + m][n][n] << endl;        

    return 0;
}

总结

在求解线性DP问题时如最大/最小注意区分它们的区别,考虑好边界问题和初始化已经求解的顺序问题等等!

参考文献:

acwing算法提高课

AtCoder Beginner Contest 232


注:如果文章有任何错误或不足,请各位大佬尽情指出,评论留言留下您宝贵的建议!如果这篇文章对你有些许帮助,希望可爱亲切的您点个赞推荐一手,非常感谢啦


欢迎访问:本人博客园地址

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值