线性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最多能够摘到多少颗花生。
输入格式第一行是一个整数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
并不是指两条路径的路径和,它只表示了上一种状态
因为i
和j
最小时,k = 2
,i
和j
最大时,k = n + n
,所以k
的总的状态范围为2
到n + 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
注:如果文章有任何错误或不足,请各位大佬尽情指出,评论留言留下您宝贵的建议!如果这篇文章对你有些许帮助,希望可爱亲切的您点个赞推荐一手,非常感谢啦
欢迎访问:本人博客园地址