蒟蒻在DP之路上求索着

T1 P1004 方格取数

题目描述

设有$N\times N$的方格图$(N≤9)$,我们将其中的某些方格中填入正整数,而其他的方格中则放入数字0。如下图所示(见样例):

A

0 0 0 0 0 0 0 0

0 0 13 0 0 6 0 0

0 0 0 0 7 0 0 0

0 0 0 14 0 0 0 0

0 21 0 0 0 4 0 0

0 0 15 0 0 0 0 0

0 14 0 0 0 0 0 0

0 0 0 0 0 0 0 0
B

某人从图的左上角的A点出发,可以向下行走,也可以向右走,直到到达右下角的B点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。
此人从A点到B点共走两次,试找出2条这样的路径,使得取得的数之和为最大。

输入格式

输入的第一行为一个整数N(表示$N \times N$的方格图),接下来的每行有三个整数,前两个表示位置,第三个数为该位置上所放的数。一行单独的0表示输入结束。

输出格式

只需输出一个整数,表示2条路径上取得的最大的和。

解法

其实如果只走一条路径完全可以直接二维DP出来。但是这题有一个特殊的地方, 那就是取走的地方会变为0.这就不允许我们分别DP。在这种情况下,我们要参考数据范围,然后发现,可以设$f[i][j][p][q]$表示第一条路走到了$(i,j)$,第二条路走到了$(p,q)$,的最大值。因为第二条路和第一条路的两个端点是一样的,所以可以当成从同一起点出发的两条路了。然后转移时就枚举两条路分别是从上面还是左面转移来的即可。

代码:

#include<iostream>
#include<cstdio>
using namespace std;

int s[10][10], n, f[10][10][10][10];

inline int max(int x, int y) {
    return x > y ? x : y;
}

int main()
{
    cin >> n;
    do {
        int y, x, c;
        cin>>y>>x>>c;
        if(y == 0 && x == 0 && c == 0) break;
        s[y][x] = c;
    }while (1);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            for (int p = 1; p <= n; p++)
                for (int q = 1; q <= n; q++) {
                    f[i][j][p][q] = max(f[i][j][p][q], f[i - 1][j][p - 1][q] + s[p][q]);
                    f[i][j][p][q] = max(f[i][j][p][q], f[i - 1][j][p][q - 1] + s[p][q]);
                    f[i][j][p][q] = max(f[i][j][p][q], f[i][j - 1][p - 1][q] + s[p][q]);
                    f[i][j][p][q] = max(f[i][j][p][q], f[i][j - 1][p][q - 1] + s[p][q]);
                    if (i != p || j != q) f[i][j][p][q] += s[i][j];
                }
    cout << f[n][n][n][n] << endl;
    return 0;
}

T2 P1541 乌龟棋

题目背景
小明过生日的时候,爸爸送给他一副乌龟棋当作礼物。

题目描述

乌龟棋的棋盘是一行N个格子,每个格子上一个分数(非负整数)。棋盘第1格是唯一的起点,第N格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。

乌龟棋中M张爬行卡片,分成4种不同的类型(M张卡片中不一定包含所有4种类型的卡片,见样例),每种类型的卡片上分别标有$1,2,3,4$四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。

游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的分数总和。

很明显,用不同的爬行卡片使用顺序会使得最终游戏的得分不同,小明想要找到一种卡片使用顺序使得最终游戏得分最多。

现在,告诉你棋盘上每个格子的分数和所有的爬行卡片,你能告诉小明,他最多能得到多少分吗?

输入格式

每行中两个数之间用一个空格隔开。

第1行2个正整数N,M,分别表示棋盘格子数和爬行卡片数。

第2行N个非负整数,a_1,a_2,…,a_N。其中a_i表示棋盘第i个格子上的分数。

第3行M个整数,b_1,b_2,…,b_M,表示M张爬行卡片上的数字。

输入数据保证到达终点时刚好用光M张爬行卡片。

输出格式

1个整数,表示小明最多能得到的分数。

解法

至此我们已经基本上看出来了四维DP的使用前提:

  1. 二维通常不符合题意
  2. 数据范围能承受的住
  3. 题目中的关键信息往往可以关联到四个种类的信息。

在这道题中,卡片的数目已经限定有且只有4种了,且每种卡片的使用互相独立,而且数据范并不大,因此我们考虑设$f[i][j][p][q]$表示第一种卡片用了i张,第二种卡片用了j张,第三种卡片用了p张,第四种卡片用了q张,所能得到的最大分数。因为题目保证所有卡片都用完,所以目标是$f[c1][c2][c3][c4]$.转移的时候直接根据用的张数就可以直接算出现在在哪一个点。

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

int n, c, c1, c2, c3, c4, f[45][45][45][45], s[355];

inline int max(int x, int y) {
    return x > y ? x : y;
} 

int main() {
    cin >> n >> c;
    for (int i = 1; i <= n; i++)
        cin >> s[i];
    for (int i = 1; i <= c; i++) {
    int x;
    cin >> x;
    if (x == 1) c1++;
    if (x == 2) c2++;
    if (x == 3) c3++;
    if (x == 4) c4++;
    }
    f[0][0][0][0] = s[1];
    if (c4 > 0) f[1][0][0][0] = s[5];
    if (c3 > 0) f[0][1][0][0] = s[4];
    if (c2 > 0) f[0][0][1][0] = s[3];
    if (c1 > 0) f[0][0][0][1] = s[2];
    for (int i = 0; i <= c4; i++)
        for (int j = 0; j <= c3; j++)
            for (int p = 0; p <= c2; p++)
                for (int q = 0; q <= c1; q++) {
                    if (i == 0 && j == 0 && p == 0 && q == 0) continue;
                    if (i > 0) f[i][j][p][q] = max(f[i][j][p][q], f[i - 1][j][p][q] + s[i * 4 + j * 3 + p * 2 + q * 1 + 1]);
                    if (j > 0) f[i][j][p][q] = max(f[i][j][p][q], f[i][j - 1][p][q] + s[i * 4 + j * 3 + p * 2 + q * 1 + 1]);
                    if (p > 0) f[i][j][p][q] = max(f[i][j][p][q], f[i][j][p - 1][q] + s[i * 4 + j * 3 + p * 2 + q * 1 + 1]);
                    if (q > 0) f[i][j][p][q] = max(f[i][j][p][q], f[i][j][p][q - 1] + s[i * 4 + j * 3 + p * 2 + q * 1 + 1]);
        }
    cout << f[c4][c3][c2][c1] << endl;
    return 0;
}

T3 P1006 传纸条

题目描述
小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。一次素质拓展活动中,班上同学安排做成一个m行n列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标$(1,1)$,小轩坐在矩阵的右下角,坐标$(m,n)$。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。

在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙。反之亦然。

还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用0表示),可以用一个0-100的自然数来表示,数越大表示越好心。小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这2条路径上同学的好心程度之和最大。现在,请你帮助小渊和小轩找到这样的2条路径。

输入格式
输入文件,第一行有2个用空格隔开的整数m和n,表示班里有m行n列。

接下来的m行是一个$m\times n$的矩阵,矩阵中第i行j列的整数表示坐在第i行j列的学生的好心程度。每行的n个整数之间用空格隔开。

输出格式
输出文件共一行,包含一个整数,表示来回2条路上参与传递纸条的学生的好心程度之和的最大值。

解法

其实这道题因为放在了这个专题里而变得很简单。如果平时看到这道题,我们很可能会首先考虑二维DP,即先找出最大的一条路,再找另一条最大的路。但是这是错误的。因为题目要求两条路没有公共点,也就是说直接找到最大的一条路有可能会使得其他答案被阻断从而出现了第一条路优但是全局不优的结果。因此我们只能考虑四维DP。设$f[i][j][p][q]$表示第一条路到了$(i,j)$,第二条路到了$(p,q)$的最大值,然后我们用类似第一题的做法来解决这个题就可以了。

代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

int n, m, f[52][52][52][52], c[50][50];

inline int max(int x, int y) {
    return x > y ? x : y;
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> c[i][j];
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            for (int p = 1; p <= n; p++)
                for (int q = j + 1; q <= m; q++) {
                    f[i][j][p][q] = max(f[i][j][p][q], f[i - 1][j][p - 1][q] + c[p][q] + c[i][j]);
                    f[i][j][p][q] = max(f[i][j][p][q], f[i][j - 1][p][q - 1] + c[p][q] + c[i][j]);
                    f[i][j][p][q] = max(f[i][j][p][q], f[i - 1][j][p][q - 1] + c[p][q] + c[i][j]);
                    f[i][j][p][q] = max(f[i][j][p][q], f[i][j - 1][p - 1][q] + c[p][q] + c[i][j]);
                }
    cout << f[n][m - 1][n - 1][m] << endl;
    return 0;
}

T4 [JSOI2008]Blue Mary的职员分配

题目描述

​ 由于Blue Mary呕心沥血的管理,Blue Mary的网络公司蒸蒸日上。现在一共拥有了n名职员,可惜没有任何的金钱和声誉。平均每名每天职员都可以给公司带来x单位金钱或者y单位声誉(名利不能双全)。并且可以花费z单位的金钱在人才交易市场发布广告招聘职员,每次发布广告三天以后就会招聘到一名职员,并且必须在发布广告并且招聘到职员的那一天才能发布下一次广告。

​ Blue Mary计划以最快的时间获得至少A单位金钱和至少B单位声誉,请你计算一下他至少需要多少时间才能达到他的目标。

数据范围与约定

对于所有的数据, $1\le n,x,y,z,A,B\le 20$.

解法

​ 这可能是我目前遇到最强的DP题了吧.

​ 题目中给了大量的信息,我们发现数据范围异常的小,因此我们最多可以把状态加到4维.接下来的转移让我怀疑人生.

​ 考虑到在现有的人中,可以有不同的人挣钱,其他人挣声誉,所以一个状态可以导致多个状态,同样的,一个状态也可以由多个状态转移过来.因此为了方便思考,选择刷表法而不是填表法.期初我试图使用三维或者二维DP,但是发现转移可能会变得非常变态,所以只能考虑四维(估计数据范围也是照着四维给的).但是这四维设什么呢?来看看题目中的信息:我们要求的是达到一定金钱和声誉的时间,所以相关的肯定要记录现在已有的金钱和声誉.而只记录这两个显然会使得状态无从转移,所以我们再加上与现在的人数有关的一维.而人数可以通过打广告来增加,所以我们需要再加一个打广告的维.

​ 看看我们得到了什么.设$f[i][a][b][j]$表示现在我们有i个人,手中有a的金钱和b的声誉,距离上一次发出广告已经过去了j天(个人怀疑这一维是怎么用人脑想出来的).然后就是转移了.显然,人数会直接影响a,b.而j的不同会直接导致i的不同.我们应该按照j来分类讨论.

if j == 0,即打广告的时刻
    f[i][a][b][0] -> f[i][下个状态的钱][下个状态的声誉][0];//相当于推迟打广告,这个时刻不打广告
    if 下个状态的金钱 >= z
        f[i][a][b][0] -> f[i][下一状态的钱-z][下个状态的声誉][1];//相当于这一时刻打了广告,前提是钱够
if j == 3,即招到人的时刻
    f[i][a][b][3] -> f[i+1][下个状态的钱][下个状态的声誉][0];//不继续打广告
    if 下个状态的金钱 >= z
        f[i][a][b][3] -> f[i+1][下个状态的钱-z][下个状态的声誉][1];//刚招到人又打了广告
else f[i][a][b][j] -> f[i][a][b][j+1];//已经打了广告,除了等到人来别无选择

这就是大致的思路.接下来可以用上面的伪代码大致对照一下实际代码来加深理解

int main() {
    read(n), read(x), read(y), read(z), read(A), read(B);
    ans = inf;
    memset(f, 0x3f, sizeof(f));
    f[n][0][0][0] = 0;
    for (int i = n; i <= 40; ++i)//四重循环
    for (int j = 0; j <= 3; ++j)//这个要放在外层,不然会WA
    for (int a = 0; a <= max(A, z); ++a)
    for (int b = 0; b <= B; ++b) {
        if (f[i][a][b][j] >= ans) continue;//劣解没必要讨论
        int nowans = f[i][a][b][j];//缩短代码长度
        if (a >= A && b >= B) {
            ans = min(ans, nowans);
            continue;
        }
        for (int k = 0; k <= i; ++k) {
            int nowa = min(a+k*x, max(A, z)), nowb = min(B, b+y*(i-k));//再次缩短代码长度
            if (j == 0) {//讨论这个状态打还是不打
                f[i][nowa][nowb][0] = min(f[i][nowa][nowb][0], nowans + 1);
                if (nowa >= z)
                    f[i][nowa-z][nowb][1] = min(f[i][nowa-z][nowb][1], nowans+1);
            }
            else if (j == 3) {//讨论这个状态打还是不打
                f[i+1][nowa][nowb][0] = min(f[i+1][nowa][nowb][0], nowans+1);
                if (nowa >= z)
                    f[i+1][nowa-z][nowb][1] = min(f[i+1][nowa-z][nowb][1], nowans + 1);
            }
            else f[i][nowa][nowb][j+1] = min(f[i][nowa][nowb][j+1], nowans + 1);
            //只能等着
        }
    }
    cout << ans << endl;
    return 0;
}

——Gensokyo