机试训练6 —— 动态规划(DP)

一、经典dp问题

1. 背包

2. 最长公共子序列(LCS)

(1)hdu 1159  Common Subsquences

    题意:求两个字符串的公共子序列

    思路:dp求公共子序列,a[i] = b[j]时,dp[i][j] = dp[i - 1][j - 1] + 1,否则dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])。

    注意:两个字符串之间可能有多个空格。dp递推时关于i = 0 / j = 0的处理。

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

char a[1005], b[1005];
int dp[1005][1005];

int main(void)
{
    char ch;
    int pb = 0;
    while(scanf("%s", &a) != EOF)
    {
        scanf("%c", &ch);
        while(ch == ' ')
            scanf("%c", &ch);
        pb = 0;
        while(ch != '\n')
        {
            if(ch != ' ')
                b[pb ++] = ch;
            scanf("%c", &ch);
        }
        b[pb] = '\0';
        int la = strlen(a), lb = strlen(b);
        memset(dp, 0, sizeof(dp));

        for(int i = 0; i < la; ++ i)
        {
            for(int j = 0; j < lb; ++ j)
            {
                if(a[i] == b[j])
                {
                    if(i == 0 || j == 0)
                        dp[i][j] = 1;
                    else
                        dp[i][j] = dp[i - 1][j - 1] + 1;
                }
                else
                {
                    if(i == 0 && j == 0)
                        dp[i][j] = 0;
                    else if(i == 0)
                        dp[i][j] = dp[i][j - 1];
                    else if(j == 0)
                        dp[i][j] = dp[i - 1][j];
                    else
                        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        printf("%d\n", dp[la - 1][lb - 1]);
    }
    return 0;
}

3. 最长递增子序列(LIS)

(1)hdu 1087  Super jumping! jumping! jumping!

    题意:给定一个正整数序列,求所有递增子序列中和最大的那个。

    思路:设sum[i]表示i之前所有递增子序列中的最大和。那么sum[i] = max{sum[j] + a[i], a[i] > a[j]}。求出所有sum后,再求sum中的最大值即可。

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

int a[1005], sum[1005];

int main(void)
{
    int n;
    while(scanf("%d", &n) != EOF && n != 0)
    {
        for(int i = 1; i <= n; ++ i)
            scanf("%d", &a[i]);
        memset(sum, 0, sizeof(sum));
        for(int i = 1; i <= n; ++ i)
            sum[i] = a[i];
        for(int i = 1; i <= n; ++ i)
        {
            for(int j = 1; j < i; ++ j)
            {
                if(a[i] > a[j])
                    sum[i] = max(sum[i], sum[j] + a[i]);
            }
        }
        int ans = 0;
        for(int i = 1; i <= n; ++ i)
            ans = max(ans, sum[i]);
        printf("%d\n", ans);
    }
    return 0;
}

(2)hdu 1003  Max Sum

    题意:给定一个整数序列,求具有最大和的子串(必须是连续的)。

    思路:设dp[i]代表以a[i]结尾的子串的最大和,那么dp[i] = max(dp[i - 1] + a[i], a[i]),由于dp[i]表示的是以a[i]结尾的所有子串中的最大和,因此dp[i]只可能是dp[i - 1] + a[i](当前数连着前面的算上)或者a[i](从当前开始一个新的子串)。

    注意:该题dp数组含义的设置非常重要,保证其无后效性。

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

int a[100005], dp[100005], l[100005];

int main(void)
{
    int tcase, n;
    scanf("%d", &tcase);
    for(int tx = 1; tx <= tcase; ++ tx)
    {
        scanf("%d", &n);
        for(int i = 1; i <= n; ++ i)
            scanf("%d", &a[i]);
        dp[1] = a[1];
        l[1] = 1;
        for(int i = 2; i <= n; ++ i)
        {
            if(dp[i - 1] + a[i] >= a[i])
            {
                dp[i] = dp[i - 1] + a[i];
                l[i] = l[i - 1];
            }
            else
            {
                dp[i] = a[i];
                l[i] = i;
            }
        }
        int ans = -0x3f3f3f3f, anx;
        for(int i = 1; i <= n; ++ i)
        {
            if(dp[i] > ans)
            {
                ans = dp[i];
                anx = i;
            }
        }
        printf("Case %d:\n", tx);
        printf("%d %d %d\n", ans, l[anx], anx);
        if(tx < tcase)
            printf("\n");
    }
    return 0;
}

二、区间dp

参考博客:博客地址

1.  nyist 737  石子合并  题目链接

    题意:n堆石子,每次只能合并相邻的两堆,合并的代价为两堆石子的总和,求将所有石子合并成一堆所需的最小代价。

    思路:dp[i][j]表示合并i到j堆石子需要的最小代价,dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]),其中w[i][j]代表第i到j堆的石子之和。

    注意:合并代价是两堆石子合并的总代价,因此dp[i][i] = 0。区间dp进行递推时,最外层枚举区间长度,然后内层枚举区间起点,最内层枚举区间的分割点。

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

int a[205], dp[205][205], ans[205];

int main(void)
{
    int n;
    while(scanf("%d", &n) != EOF)
    {
        ans[0] = 0;
        for(int i = 1; i <= n; ++ i)
        {
            scanf("%d", &a[i]);
            ans[i] = ans[i - 1] + a[i];
        }
        for(int i = 1; i <= n; ++ i)
        {
            for(int j = 1; j <= n; ++ j)
            {
                if(i == j)
                    dp[i][j] = 0;
                else
                    dp[i][j] = 0x3f3f3f3f;
            }
        }
        for(int i = 2; i <= n; ++ i)
        {
            for(int j = 1; j <= n; ++ j)
            {
                int r = j + i - 1;
                if(r > n)  break;
                for(int k = j; k < r; ++ k)
                {
                    dp[j][r] = min(dp[j][r], dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1]);
                }
            }
        }
        printf("%d\n", dp[1][n]);
    }
    return 0;
}

四边形优化 —— 结论记住     参考博客链接:   参考博客        O(n^3)优化至O(n^2)

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

int a[205], dp[205][205], ans[205], s[205][205];

int main(void)
{
    int n;
    while(scanf("%d", &n) != EOF)
    {
        ans[0] = 0;
        for(int i = 1; i <= n; ++ i)
        {
            scanf("%d", &a[i]);
            ans[i] = ans[i - 1] + a[i];
        }
        for(int i = 1; i <= n; ++ i)
        {
            for(int j = 1; j <= n; ++ j)
            {
                if(i == j)
                    dp[i][j] = 0;
                else
                    dp[i][j] = 0x3f3f3f3f;
            }
        }
        for(int i = 1; i <= n; ++ i)
            s[i][i] = i;
        for(int i = 2; i <= n; ++ i)
        {
            for(int j = 1; j <= n; ++ j)
            {
                int r = j + i - 1;
                if(r > n)  break;
                for(int k = s[j][r - 1]; k <= s[j + 1][r]; ++ k)
                {
                    if(dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1] < dp[j][r])
                    {
                        dp[j][r] = dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1];
                        s[j][r] = k;
                    }
                }
            }
        }
        printf("%d\n", dp[1][n]);
    }
    return 0;
}

2. hdu 3506  Monkey Party

    题意:问题可转化为n堆石子排成1个圈,将其合并成1堆,求合并的最小代价。

    思路:原来的石子合并是n堆排成一条线,现在是一个圈。将前n - 1堆石子加到n堆石子的后面,构成一个2 * n - 1堆石子的链,求合成一堆的最小值,就是求dp[i][i + n - 1], i = 1,2,3.... n的最小值,因为对于一个圈来讲,展开以后,每一个点都可以看作是对应线段的左端点。问题转化好之后,可以使用与上一道题相同的方法求解,要用四边形优化。

    注意:(1)环展成线以后,数组大小要翻倍。(2)在dp递推时,枚举顶点的时候,要从1枚举到2*n-1,而不是只枚举到n,因为对于不从顶点1展开的环来讲,它在整个2*n-1的线段上会覆盖从i到i+n-1这段线段,其中有超出n的部分,因此分割点可能会超过n,所以起点枚举必须到2*n-1。

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

int a[2005], dp[2005][2005], ans[2005], s[2005][2005];

int main(void)
{
    int n;
    while(scanf("%d", &n) != EOF)
    {
        ans[0] = 0;
        for(int i = 1; i <= n; ++ i)
        {
            scanf("%d", &a[i]);
            ans[i] = ans[i - 1] + a[i];
        }
        for(int i = n + 1; i <= 2 * n - 1; ++ i)
        {
            a[i] = a[i - n];
            ans[i] = ans[i - 1] + a[i];
        }
        for(int i = 1; i <= 2 * n - 1; ++ i)
        {
            for(int j = 1; j <= 2 * n - 1; ++ j)
            {
                if(i == j)
                {
                    dp[i][j] = 0;
                    s[i][j] = i;
                }
                else
                    dp[i][j] = 0x3f3f3f3f;
            }
        }
        for(int i = 2; i <= n; ++ i)
        {
            for(int j = 1; j <= 2 * n - 1; ++ j)
            {
                int r = j + i - 1;
                if(r > 2 * n - 1)  break;
                for(int k = s[j][r - 1]; k <= s[j + 1][r]; ++ k)
                {
                    if(dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1] < dp[j][r])
                    {
                        dp[j][r] = dp[j][k] + dp[k + 1][r] + ans[r] - ans[j - 1];
                        s[j][r] = k;
                    }
                }
            }
        }
        int ans = 0x3f3f3f3f;
        for(int i = 1; i <= n; ++ i)
            ans = min(ans, dp[i][i + n - 1]);
        printf("%d\n", ans);
    }
    return 0;
}

3. poj 2955  Brackets

    题意:已知'()'和'[]'是匹配的括号形式,求给定字符串中最大的匹配序列长度。

    思路:设dp[i][j]代表区间[i,j]上的最大匹配长度。如果a[i]和a[r]是匹配的,如果[i,r]上的最大匹配长度子序列中i和r恰好匹配,那么dp[i][r] = dp[i + 1][r - 1] + 2,然后再枚举区间[i,r]上的每一个分割点,取最大值,即dp[i][r] = max(dp[i][r], dp[i][k] + dp[k + 1][r])。

    注意:不论a[i]和a[r]是否恰好匹配,对于每一个区间[i, r],枚举区间中的每一个分割点并取最大值都是必须的。因为,如果a[i]和a[r]匹配,dp[i][r] = dp[i + 1][r - 1] + 2代表最大长度的匹配子序列中a[i]和a[r]匹配,但是这种情况下有可能最大长度的匹配子序列并不是a[i]和a[r]进行匹配,而是他们分别与区间中的其他位置匹配,然后取得了更大的长度。比如序列( ) [ ] ( ),虽然第一个和最后一个字符是匹配的,但显然在最大长度的匹配子序列里面,他们并不是相互匹配的那一对,而是分别和自己相邻的括号进行匹配,这样取得的才是最大的匹配长度。

#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

char a[105];
int dp[105][105];

int main(void)
{
    int la = 0;
    while(scanf("%s", a) != EOF && strcmp(a, "end") != 0)
    {
        la = strlen(a);
        memset(dp, 0, sizeof(dp));
        for(int len = 2; len <= la; ++ len)
        {
            for(int i = 0; i < la; ++ i)
            {
                int r = i + len - 1;
                if(r >= la)
                    break;
                if((a[i] == '(' && a[r] == ')') || a[i] == '[' && a[r] == ']')
                {
                    if(r - i >= 2)
                        dp[i][r] = dp[i + 1][r - 1] + 2;
                    else
                        dp[i][r] = 2;
                }
                for(int k = i; k < r; ++ k)
                    dp[i][r] = max(dp[i][r], dp[i][k] + dp[k + 1][r]);
            }
        }
        printf("%d\n", dp[0][la - 1]);
    }
    return 0;
}

4. 整数划分问题

    给定整数n,m,在n中添加m个乘号将n分成m个部分,求可以得到的最大乘积。

    思路:设dp[i][j]代表第i位之前插入了j个乘号所能得到的最大乘积。接下来,我们要枚举乘号添加的位置,dp[i][j] = max(dp[i][j], dp[k][j - 1] * num[k + 1][i]),其中num[i][j]代表从i位到j位所代表的的数值。

    具体代码实现可见如下链接  整数划分部分内容

5. 凸多边形三角划分问题

    给定整数n,每个顶点都有一个权值,将凸n边形划分为n-2个三角形,求所有三角形各顶点乘积的和的最小值。

    思路:设dp[i][j]表示从顶点i到顶点j构成的凸多边形对应的乘积和的最小值。枚举凸多边形的分割点k,则有dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + a[i] * a[j] * a[k])。

    具体代码实现可见如下链接  凸多边形三角划分部分

6. hdu 2513  Cake Slicing

    题意:n*m的矩形蛋糕,某些单元内放置樱桃,每次切割蛋糕时必须沿水平或垂直方向切割并且切到底将蛋糕分成两部分,要求经过若干次切割后每部分蛋糕中只含有一个樱桃,求各次切割的最小切割长度之和。

    思路:设dp[a][b][c][d]代表以(a,b)为左下角(c,d)为右上角的蛋糕切割成各部分只含有一个樱桃的最小切割长度和,接下来枚举切割的位置,可能沿水平方向切割,可能沿垂直方向切割,取所有情况中的最小的那个。这里采用深搜的方法来写dp即记忆化搜索。

    记忆化搜索碰到已算过的dp值则直接返回,碰到没计算过的才会继续向下递归计算,相当于dp的递归形式。

    注意:这里注意每个坐标代表的是该单元的中心位置而不是某一单元的某顶点坐标,如(1,1)就代表最左下那个单元格。注意枚举切割位置时的上下界。

#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

int dp[25][25][25][25], mark[25][25];

int dfs(int a, int b, int c, int d)
{
    if(dp[a][b][c][d] != -1)
        return dp[a][b][c][d];
    int ans = 0;
    for(int i = a; i <= c; ++ i)
    {
        for(int j = b; j <= d; ++ j)
        {
            if(mark[i][j] == 1)
                ans ++;
        }
    }
    if(ans <= 1)
        return dp[a][b][c][d] = 0;
    int mins = 0x3f3f3f3f;
    for(int i = b; i < d; ++ i)
        mins = min(mins, dfs(a, b, c, i) + dfs(a, i + 1, c, d) + c - a + 1);
    for(int i = a; i < c; ++ i)
        mins = min(mins, dfs(a, b, i, d) + dfs(i + 1, b, c, d) + d - b + 1);
    return dp[a][b][c][d] = mins;
}

int main(void)
{
    int n, m, k, a, b, tcase = 1;
    while(scanf("%d%d%d", &n, &m, &k) != EOF)
    {
        memset(mark, 0, sizeof(mark));
        memset(dp, -1, sizeof(dp));    //memset仅能初始化为0或者-1
        for(int i = 1; i <= k; ++ i)
        {
            scanf("%d%d", &a, &b);
            mark[a][b] = 1;
        }
        int ans = dfs(1, 1, n, n);
        printf("Case %d: %d\n", tcase, ans);
        tcase ++;
    }
    return 0;
}

区间dp的求解思路

    对于区间dp,可以这样考虑,对于区间[i, j],dp[i][j]可以怎样由更小的区间得到,枚举区间的分割点将区间分成更小的部分使得dp[i][j]可由更小区间上的dp值来递推得到,而区间的分割点在各个问题中通常都有着它对应的实际意义。

三、树形dp

参考博客地址   树形dp参考博客地址

1. hdu 1520  Anniversary party

    题意:每个人都有有唯一一个直接管理者,每个人都有一个权重,从所有人中选取若干人,使得每个人和他的直接管理者不同时出现,选出一些人使权重总值最大。

    思路:所有人构成一个树形结构,求在树上选出一些结点,使得结点和他的父节点不同时出现,求最大权值和。设dp[i][0]表示以i为根结点且不选i的子树最大权值和,dp[i][1]表示以i为根结点且选i的子树最大权值和。则,dp[i][1] = sum{dp[j][0]} + a[i],dp[i][0] = sum{max(dp[j][0], dp[j][1])},其中j为i的所有子结点,使用递归计算即可。

    注意:树形dp在递推过程中,对于每个结点的状态可以再开一维来表示,比如这里第二维表示该结点是否选择。

#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <queue>
#include <vector>
#include <algorithm>

using namespace std;

vector <int> g[6005];
int a[6005], dp[6005][2], indeg[6005];

int dfs(int x, int op)
{
    if(dp[x][op] != -1)
        return dp[x][op];
    dp[x][1] = a[x];
    dp[x][0] = 0;
    for(int i = 0; i < g[x].size(); ++ i)
    {
        int v = g[x][i];
        dp[x][1] += dfs(v, 0);
        dp[x][0] += max(dfs(v, 0), dfs(v, 1));
    }
    return dp[x][op];
}

int main(void)
{
    int n, u, v;
    while(scanf("%d", &n) != EOF)
    {
        memset(dp, -1, sizeof(dp));
        memset(indeg, 0, sizeof(indeg));
        for(int i = 1; i <= n; ++ i)
            scanf("%d", &a[i]);
        for(int i = 1; i <= n; ++ i)
            g[i].clear();
        scanf("%d%d", &u, &v);
        while(u + v != 0)
        {
            g[v].push_back(u);
            indeg[u] ++;
            scanf("%d%d", &u, &v);
        }
        int root = 0;
        for(int i = 1; i <= n; ++ i)
        {
            if(indeg[i] == 0)
            {
                root = i;
                break;
            }
        }
        int ans = max(dfs(root, 0), dfs(root, 1));
        printf("%d\n", ans);

    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_33872397/article/details/82526522
今日推荐