经典dp问题 矩阵取数以及变形

再次总结一些比较经典的在矩阵中确定一个起始状态和结束状态, 问从起始到结束中走过的数要得到, 问最小(大), 路径输出等问题. 这些问题都可以转化为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);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值