再次总结一些比较经典的在矩阵中确定一个起始状态和结束状态, 问从起始到结束中走过的数要得到, 问最小(大), 路径输出等问题. 这些问题都可以转化为dp模型来解决. 下面以一些我做过的题进行分析.
入门级: 51Nod - 1083
问题: 给定一个二维矩阵, 问从左上角走到右下角可以取到的最大值是多少. 每次只能往下或者往右走.
这道题就非常简单了, dp[i][j] 代表走到(i, j)这个点的最大值是多少, 转移方程就是
dp[i][j] = dp[i-1][j] + dp[i][j-1]; O(n^2)的复杂度.
AC Code
const int maxn = 5e2+5;
int a[maxn][maxn], dp[maxn][maxn];
void solve()
{
int n;
cin >> n;
for (int i = 1 ; i <= n ; i ++) {
for (int j = 1 ; j <= n ; j ++) {
scanf("%d", &a[i][j]);
}
}
for (int i = 1 ; i <= n ; i ++) {
for (int j = 1 ; j <= n ; j ++) {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + a[i][j];
}
}
cout << dp[n][n] << endl;
}
变形题 洛谷P1002
题意: 就是在一个二维矩阵中, 有一匹马, 马一次能跳到的点是他的控制点, 问从(0, 0)出发到达所给的终点的路径有多少条. 每次只能向下或者右走.
依旧dp, dp[i][j] 代表到达这个点的路径条数, 那么它依旧只能被上走两个进行更新, 只不过我们需要处理下马搜控制的点判一下不进行更新即可. 转移方程类似
AC Code
const int maxn = 50+5;
int a[maxn][maxn];
ll dp[maxn][maxn];
int dx[] = {-1, -1, -2, -2, 1, 1, 2, 2};
int dy[] = {2, -2, 1, -1, 2, -2, 1, -1};
void solve()
{
int ex, ey, rx, ry;
cin >> ex >> ey >> rx >> ry;
for (int i = 0 ; i < 8 ; i ++) {
int xx = rx + dx[i];
int yy = ry + dy[i];
if (xx < 0 || yy < 0) continue;
a[xx][yy] = -1;
}
a[rx][ry] = -1;
dp[0][0] = 1;
for (int i = 0 ; i <= 20 ; i ++) {
for (int j = 0 ; j <= 20 ; j ++) {
if (a[i][j] == -1) continue;
if (i) dp[i][j] = dp[i-1][j];
if (j) dp[i][j] += dp[i][j-1];
}
if (dp[ex][ey]) break;
}
printf("%lld\n", dp[ex][ey]);
}
变形二: Uva - 10285
题意: 还是一个二维矩阵, 可以从任意一个点出发或者结束, 对应上面的点代表该点的海波高度, 每次只能向海拔高度低的走, 问可以走的最长的路有多长. 可以往四个方向走.
思路: 还是dp, dp[i][j] 代表走到这个点的最长路径是多少, 那么因为起始点可以任意, 所以我们需要进行一波预处理, 即我们把矩阵中的每一个数和位置先取出来, 然后按照从小到大进行排序, 每次取出来一个就进行在原图上的dp, 即判断四个方向中如果有比当前点小的点并且可以进行更新(即更优路径), 那么我们就更新这个点即可, 最后在所有的点中取一个max即可
AC Code
const int maxn = 1e2+5;
int dx[] = {1, -1, 0, 0};
int dy[] = {0, 0, 1, -1};
struct node {
int x, y, w;
bool operator <( const node& _) const {
return w < _.w;
}
}e[maxn*maxn];
int a[maxn][maxn], dp[maxn][maxn];
void solve()
{
string s; int n, m;
while(cin >> s >> n >> m) {
int k = 0; Fill(a, -1); Fill(dp, 0);
for (int i = 1 ; i <= n ; i ++) {
for (int j = 1 ; j <= m ; j ++) {
int x; scanf("%d", &x);
e[++k] = node{i, j, x};
}
}
sort(e+1, e+1+k); int ans = 0;
for (int i = 1 ; i <= k ; i ++) {
int x = e[i].x, y = e[i].y;
a[x][y] = e[i].w; dp[x][y] = 1;
for (int j = 0 ; j < 4 ; j ++) {
int tmp = a[x+dx[j]][y+dy[j]];
int val = dp[x+dx[j]][y+dy[j]] + 1;
if (tmp != -1 && tmp < e[i].w && val > dp[x][y]) {
dp[x][y] = val;
ans = max(ans, dp[x][y]);
}
}
}
cout << s << ": " << ans << endl;
}
}
经典问题 : Uva116
(这道题非常经典, 一定要记住这道题!)
题意: 还是一个二维矩阵, 每个点上有数, 从这个矩阵的第一列的任意位置出发到达最后一列的任意位置结束, 每次可以向下一列的上面一行, 当前行, 下面一行走, 并且注意在第一行时向上面一行走是走到最后一行, 同理最后一行也是, 问能取到的最小值是多少, 并且输出路径, 即每一列的行数, 如果有多解, 那么输出字典序最小的答案.
思路: 跟矩阵取数有点类似, 所以我们可以设dp[i][j]代表走到这个(i, j)的最小值是多少, 注意到最后要输出路径, 当然很简单记录一下此时的状态从哪个状态转移过来的即可, (但是我正着写死活过不了, 希望路过的大神贴一份正着写的代码给我学习学习…), 所以我们倒着推, 即从结束状态往起始状态推, 这样答案肯定不会变的,然后我们处理好边界问题即可, 和同时处理好字典序的问题, 详情请看代码实现:
AC Code
const int inf = 0x3f3f3f3f; //用这个可以直接mem
const ll INF = 1e18;
const int mod = 1e9+7;
const int maxn = 1e2+5;
int a[15][maxn], pre[15][maxn], dp[15][maxn];
int Next[15][maxn];
int path[maxn];
void solve()
{
int n, m;
while(~scanf("%d%d", &n, &m)) {
for (int i = 1 ; i <= n ; i ++) {
for (int j = 1 ; j <= m ; j ++) {
scanf("%d", &a[i][j]);
}
}
int st = 1;
Fill(dp, inf); int ans = inf;
for (int i = m ; i >= 1 ; i --) { // 倒着推, 方便记录路径
for (int j = 1 ; j <= n ; j ++) {
if (i == m) dp[j][i] = a[j][i]; // 更新初始状态
else {
int r[3] = {j-1, j, j+1};
if (j == 1) r[0] = n; // 处理边界问题
if (j == n) r[2] = 1;
sort(r, r+3); // 这样就可以保证字典序最小啦.
for (int k = 0 ; k < 3 ; k ++) {
if (dp[r[k]][i+1] + a[j][i] < dp[j][i]) {
dp[j][i] = dp[r[k]][i+1] + a[j][i];
Next[j][i] = r[k];
}
}
}
if (i == 1 && dp[j][i] < ans) ans = dp[j][i], st = j;
// 同样我们行是从小枚举的, 这样也就可以保证字典序乐.
}
}
printf("%d", st);
for (int i = 1 ; i < m ; i ++) {
printf(" %d", Next[st][i]);
st = Next[st][i];
} // 打印路径即可
printf("\n%d\n", ans);
}
}
相似题: HDU - 5092
经典题的简化, 几乎和上面那道题一模一样, 就是变成了从第一行到最后一行, 并且字典序最大, 最后输出每一行的列数即可. 那么会了上面那道, 这道就是随便写写乐, 注意保持字典序最大即可.
这还是上海邀请赛的题, 应该是改编上面那道的, 所以记住经典模型是多么重要的啊……. (当时我用的bfs写的, 贼麻烦)
AC Code
const int inf = 0x3f3f3f3f; //用这个可以直接mem
const ll INF = 1e18;
const int mod = 1e9+7;
const int maxn = 100+5;
int a[maxn][maxn];
int dp[maxn][maxn], Next[maxn][maxn];
void solve()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1 ; i <= n ; i ++) {
for (int j = 1 ; j <= m ; j ++) {
scanf("%d", &a[i][j]);
}
}
int st = 1, mi = inf; Fill(dp, inf);
for (int i = n ; i >= 1 ; i --) { // 因为要打印路径, 所以还是倒着推.
for (int j = m ; j >= 1 ; j --) { // 倒着可以保证起始点字典序最大.
if (i == n) dp[i][j] = a[i][j];
else { // 为了字典序最大就这样更新即可.
if (j + 1 <= m && dp[i+1][j+1] + a[i][j] < dp[i][j]) {
dp[i][j] = dp[i+1][j+1] + a[i][j];
Next[i][j] = j+1;
}
if (dp[i+1][j] + a[i][j] < dp[i][j]) {
dp[i][j] = dp[i+1][j] + a[i][j];
Next[i][j] = j;
}
if (j - 1 >= 1 && dp[i+1][j-1] + a[i][j] < dp[i][j]) {
dp[i][j] = dp[i+1][j-1] + a[i][j];
Next[i][j] = j-1;
}
}
if (i == 1 && dp[i][j] < mi) mi = dp[i][j], st = j;
}
}
printf("%d", st);
for (int i = 1 ; i < n ; i ++) {
st = Next[i][st];
printf(" %d", st);
}