kuangbin专题十二基础dp总结

做这个专题的时候感觉好迷。一度被题恶心到了。。
这题把所有不是独立思考做出来的题贴出来吧。
A - Max Sum Plus Plus
题解:dp[i][j] 代表前i个数在必须选第i个的前提下组成j组的最大值。
那么方程为:
dp[i][j]=max(dp[i-1][j]+a[i],dp[k][j-1]+a[i])(k<=i-1&&k>=j-1).
由于n特别大,我们不可能开二维数组。
其次,我们再看它的复杂度,O(n*m*n),肯定超时。
所以我们要做出这个题首先要解决这两个问题。
我们想一想,根据它的拓扑序,我们计算的时候可以把i放在第一次,j放在第二层,也可以倒过来放。那么倒过来放后我们可以发现dp[i][j] 会由它上一个计算的值以及上一层(也就是j-1层)的max(dp[k][j-1] (k<=i-1&&k>=j-1))得出。既然只由这两个得出,我们就可以只开一个一维数组dp[n]来存当前层的答案,再开一个mx[n]数组存上一层的极值即可。
这样转换后,空间及时间都不会超了。
注意mx数组的修改顺序,因为当计算第
i个数时,我们需要mx[i-1]的值,所以mx[i-1]的值需要在用完之后才能改。

#include
#include
#include
#include
using namespace std;
const int maxn = 1e6 + 5;
const int inf = 0x3f3f3f3f;
int a[maxn], dp[maxn], mx[maxn];

int main()
{
    int m, n;
    while (~scanf("%d %d", &m, &n))
    {
        for (int i = 1;i <= n;i++)scanf("%d", &a[i]);
        memset(dp, 0, sizeof(dp));
        memset(mx, 0, sizeof(mx));
        int Max;
        for (int j = 1;j <= m;j++)
        {
            Max = -inf;
            for (int i = j;i <= n;i++)
            {
                dp[i] = max(dp[i - 1] + a[i], mx[i - 1] + a[i]);
                mx[i - 1] = Max;
                Max = max(Max, dp[i]);
            }
        }
        printf("%d\n", Max);

    }

    return 0;

}

C - Monkey and Banana
一开始做这个题的时候,想到的是记忆化搜索,但好像写搓了,用长宽来做状态,mle了(现在想想好傻)。
之后就对方块排个序,就用dp[n]来进行递推。

#include
#include
#include
#include
using namespace std;
const int inf = 0x3f3f3f3f;

struct Qube
{
    int x, y, z;
    Qube(int _x, int _y, int _z)
    {
        if (_x < _y)swap(_x, _y);
        x = _x, y = _y, z = _z;
    }
    Qube(){}
    bool operator<(const Qube &b)const
    {
        if (x != b.x)return x > b.x;
        return y > b.y;
    }
}Q[100];
int dp[100];
int main()
{
    int n;
    int cas = 1;
    while (~scanf("%d", &n)&&n)
    {
        int x, y, z;
        int now = 0;
        for (int i = 1;i <= n;i++)
        {
            scanf("%d %d %d", &x, &y, &z);
            Q[++now] = Qube(x, y, z);
            Q[++now] = Qube(x, z, y);
            Q[++now] = Qube(y, z, x);
        }
        sort(Q + 1, Q + 1 + now);
        int ans = 0;
        for (int i = now;i >= 1;i--)
        {
            dp[i] = Q[i].z;
            for (int j = i+1;j <= now;j++)if(Q[i].x>Q[j].x&&Q[i].y>Q[j].y)
            {
                dp[i] = max(dp[i], dp[j] + Q[i].z);
            }
            ans = max(ans, dp[i]);
        }
        printf("Case %d: maximum height = %d\n", cas++, ans);
    }
    return 0;


}

D - Doing Homework
这题一点思路都没有,之后看题解才发现是状压。确实状压很有道理。

#include
#include
#include
#include
using namespace std;
const int inf = 0x3f3f3f3f;
struct node
{
    char str[100];
    int D, C;
}cls[20];
int dp[1 << 15], pre[1 << 15];
int n;
void out(int num)
{
    if (!num)return;
    int tmp;
    for (int i = 0;i < n;i++)
    {
        if (((num >> i) & 1) && !((pre[num] >> i) & 1))
        {
            tmp = i;
            break;
        }
    }
    out(pre[num]);
    printf("%s\n", cls[tmp].str);
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
    {

        scanf("%d", &n);
        memset(dp, inf, sizeof(dp));
        //memset(pre, -1, sizeof(pre));
        for (int i = 0;i < n;i++)
            scanf("%s %d %d", cls[i].str, &cls[i].D, &cls[i].C);
        dp[0] = 0;
        for (int i = 0;i < (1 << n);i++)
        {
            for (int j = 0;j < n;j++)if (!((i >> j) & 1))
            {
                int cnt = 0;
                for (int k = 0;k < n;k++)if ((i >> k) & 1)
                    cnt += cls[k].C;
                cnt += cls[j].C;
                if (cnt > cls[j].D)cnt -= cls[j].D;
                else cnt = 0;
                if (dp[i + (1 << j)] > dp[i] + cnt)
                {
                    dp[i + (1 << j)] = dp[i] + cnt;
                    pre[i + (1 << j)] = i;
                }
            }
        }
        printf("%d\n", dp[(1 << n) - 1]);
        out((1 << n) - 1);
    }
}

I - 最少拦截系统
一开始并不值得怎么建状态,看了别人说贪心可以做,觉得很有道理就用贪心写了。
我们可以用一个数组B来维护每个系统的最后一个值,也就是每个系统最小的值。然后将B从小到大排序(理解一下为什么从小到大),然后判断这个值能否加到B里面的系统中,如果不能就系统数++ 。

#include
#include
#include
#include
using namespace std;
int B[1005];
int main()
{
    int n;
    while (~scanf("%d",&n))
    {
        int bcount = 0;
        int tmp;
        for (int i = 1;i <= n;i++)
        {
            scanf("%d", &tmp);
            int j;
            for (j = 1;j <= bcount;j++)
            {
                if (B[j] >= tmp)
                {
                    B[j] = tmp;
                    break;
                }
            }
            if (j == bcount + 1)
            {
                B[++bcount] = tmp;
            }
            sort(B + 1, B + 1 + bcount);
        }
        printf("%d\n", bcount);
    }
    return 0;
}

写博客的时候看别人写的才发现 它的本质是最长上升子序列,因为最长上升子序列=序列中不上升的序列的个数。(之后会在另一篇博客详细说明).

K - Jury Compromise
这题确实比较有意思。一开始建状态还是不会建,但看完别人的数组就知道怎么做了,果然还是不会写题。。
我们来分析一下别人的做法(错误做法):
首先用二维数组dp[i][j] 来表示取i个人,他们的差为j,和最大的那个方案的和(差就代表D-P,和就代表D+P)。
那么dp[i][j]=max(dp[i-1][j-del[k]]+sum[k])。
明显可以发现上述式子有3层循环,第一层i,第二层j,再循环k。我们再加人的时候又得需要记录路径看之前是否已经加过这个人。
一眼看过去确实有点别扭,但也说不出哪里有问题。因为这个题目和01背包有点类似,只不过是加入了选择个数的限制而已。我们在做01背包的时候,首先把物品个数放在最前面,这样就能保证每个阶段都是只考虑一个物品,所以每个物品并不会放两次。而这题写法却和无限背包一样,把物品个数放在最下面,这样毋庸置疑肯定会选重复,为了避免这个出现,他就做一个判断是否之前选过这个来把这种情况去除掉。但这里有一些疑问:比如之前已经有一个最优方案选了A方案,但其实选B方案和选A的贡献是一样的。那么下一次我们在找最优方案时,假设一个元素c在A中,它和B方案可以组成最优方案,但B方案的位置已经被A占走了,那么就永远找不到最优方案了。
这里有一组数据
9 6
6 2
16 10
4 9
19 8
17 12
4 7
10 2
2 14
5 18
0 0
这组数据的答案是

Jury #1

Best jury has value 54 for prosecution and value 54 for defence:

1 2 3 4 6 9

但是错误程序的答案是

Jury #1
Best jury has value 52 for prosecution and value 52 for defence:
1 3 4 5 6 8

当正解和错误答案dp[5][-4+fixd] 都等于100(正解选择2,3,4,6,9,而错误程序选择了1,2,4,8,9),所以正解会选上1,变成总和108,但错误程序之前就选了1,那么它永远找不到这个答案了。
说了那么多,实际上这题就是01背包的变形题,按照01背包的写法即可做出来。
输出路径的代码希望大家可以理解理解,这里不再赘述了。

#include
#include
#include
#include
using namespace std;
const int maxn = 205;
int D[maxn], P[maxn];
int Deldp[maxn], Sumdp[maxn];
int dp[maxn][25][1000];
int path[maxn][25][1000];

void out(int a, int b, int idx)
{
    if (b == 0)return;
    int now = path[a][b][idx];
    if (a != now)
        out(a - 1, b, idx);
    else
    {
        out(a - 1, b - 1, idx - Deldp[now]);
        printf(" %d", now);
    }
}

int main()
{
    int n, m;
    int cas = 1;
    while (~scanf("%d %d",&n,&m)&&n)
    {
        memset(dp, -1, sizeof(dp));
        int fixd = 400;
        for (int i = 1;i <= n;i++)
        {
            scanf("%d %d", &D[i], &P[i]);
            Deldp[i] = D[i] - P[i];
            Sumdp[i] = D[i] + P[i];
        }
        for (int i = 0;i <= n;i++)
            dp[i][0][fixd] = 0;
    //  dp[0][0][fixd] = 0;
        for (int i = 1;i <= n;i++)
            for(int j=1;j<=min(i,m);j++)
                for (int k = 0;k <= 800;k++)
                {
                    dp[i][j][k] = dp[i - 1][j][k];
                    path[i][j][k] = path[i - 1][j][k];
                    if (k >= Deldp[i] && dp[i - 1][j - 1][k - Deldp[i]] != -1)
                    {

                        if (dp[i][j][k] < dp[i - 1][j - 1][k - Deldp[i]] + Sumdp[i])
                        {
                            dp[i][j][k] = dp[i - 1][j - 1][k - Deldp[i]] + Sumdp[i];

                            path[i][j][k] = i;
                        }

                    }
                    //if (dp[i][j][k] != -1)
                        //printf("%d %d %d %d\n", i, j, k, dp[i][j][k]);
                }

        int i = 0;
        while (dp[n][m][fixd + i] == -1 && dp[n][m][fixd - i] == -1)
        {
            i++;
        }
        int idx = dp[n][m][fixd + i] > dp[n][m][fixd - i] ? fixd + i : fixd - i;
        printf("Jury #%d\n", cas++);
        printf("Best jury has value %d for prosecution and value %d for defence:\n", (dp[n][m][idx] + (idx - fixd)) / 2, (dp[n][m][idx] - ( idx - fixd)) / 2);
        out(n, m, idx);

        puts("");
    }
    return 0;

}

Q - Phalanx
一开始脑子抽了写了一个三维dp[i][j][k],在第i行j列长度k是否可行。之后想了想实际dp[i][j] 就行了,果然我很菜。

#include
#include
#include
#include
using namespace std;
char map[1005][1005];
int dp[1005][1005];
int n;
int check(int x, int y)
{
    int num = 0;
    while (y-num>=1&&x+num<=n&&map[x][y - num] == map[x + num][y])
        num++;
    return num;
}
int main()
{

    while (~scanf("%d",&n)&&n)
    {
        memset(dp, 0, sizeof(dp));
        for (int i = 1;i <= n;i++)
            scanf("%s", map[i] + 1);
        //for (int i = 1;i <= n;i++)dp[i][1] = dp[n][i] = 1;
        int ans = 0;
        for(int i=n;i>=1;i--)
            for (int j = 1;j <= n;j++)
            {
                int tmp = check(i, j);
                if (tmp > dp[i + 1][j - 1])
                    dp[i][j] = dp[i + 1][j - 1] + 1;
                else
                    dp[i][j] = tmp;
                ans = max(ans, dp[i][j]);
            }
        //if (ans == 1)ans = 0;
        printf("%d\n", ans);
    }
}

S - Making the Grade
挺好的题,一开始同样没想到如何递推。。
我们先做非严格递增的情况。
设dp[i][j] 为把第i个数变成j的最小价值。
那么递推方程为:
dp[i][j] = abs(a[i]-j)+min(dp[i-1][k])(k<=j)
如果理解了A题,是不是感觉似曾相识。
我们同样面临两个问题,一是数组太大开不下去,而是复杂度O(n*1e9*1e9),肯定不可行。
首先我们可以用A题的做法把最里面的一层优化掉,那么复杂度为O(N*1e9),仍然不可行,该怎么办呢。
接下来的定理博主并不能证明,但可以大概讲解其中的奥妙。
假如数据为10 3,那么我们要么把10变成3,要么把3变成10。花费为7。那么当数据为10 7 4呢,读者可以思考一下,多试几组数据就会发现修改完后的非严格递增序列的数都为原来序列中的数。为什么会这样呢,大家可以画一个一维坐标轴,然后原来的序列的数会把坐标轴分成很多块,我们在比较两个数的时候往往带一个等号,那么其中一个数只会向他靠拢,而不会超过他,最小花费也就是把这个数变成另一个在序列中的数。
所以既然有这个定理,那么这个问题就迎刃而解啦,只要把数字离散化一下,不论时间还是空间的问题都被解决了。

#include
#include
#include
#include

using namespace std;
typedef long long ll;
const ll inf = 0x3f3f3f3f3f;

int a[2005],b[2005];
ll dp[2005][2005];
int main()
{
    int N;
    scanf("%d", &N);
    for (int i = 1;i <= N;i++)scanf("%d", &a[i]), b[i] = a[i];
    sort(a + 1, a + 1 + N);
    int Size = unique(a + 1, a + 1 + N) - a - 1;
    for (int i = 1;i <= N;i++)
    {
        ll mn = inf;
        for (int j = 1;j <= Size;j++)
        {
            if (i != 1)
            {
                mn = min(mn, dp[i - 1][j]);
                dp[i][j] = abs(b[i] - a[j]) + mn;
            }
            else
                dp[i][j] = abs(b[i] - a[j]);
        }
    }
    ll ans = inf;
    for (int i = 1;i <= Size;i++)
        ans = min(ans, dp[N][i]);
    cout << ans << endl;

}

博主在做这个专题时,一度心态爆炸,在做K题时写了3个小时,中途一直想放弃这个题,但明明知道怎么做,就是越写越慢,越写越慢。。
写dp时,博主常常纠结2个东西,一是初始化,二是式子。
dp还需要加强啊!!

你可能感兴趣的:(kuangbin专题总结)