zoukankan      html  css  js  c++  java
  • 动态规划题四类题型杂七杂八合计分析(只分析状态转移方程,AC代码自行百度) 挖地雷/合唱队形/山区建学校/剑客决斗/石子合并/加分二叉树/统计单词个数

    #include <iostream>
    #include <stack>
    using namespace std;
    
    
    int boomNum[1000];
    bool isAccess[100][100];//[i][j]=true 表示 地窖i、j间是连接的
    int m[1000];//表示从第i个地窖开始挖的最多地雷数
    
    
    int main() {
    
        
    
        int n;//n个地窖
        cin >> n;
        for (int i = 1; i <= n; i++) {
            cin >> boomNum[i]; //输入每个地窖有多少个雷
        }
    
        int start = -1, end = -1;
        //填m表 初始化 输入若干行 最后一行00表示结束
        while (!(start == 0 && end == 0)) {
            cin >> start >> end;
            isAccess[start][end] = true;
        }
        
        //从最大的num开始
        //初始化
        int max = -1;
        m[n] = boomNum[n];//固定末尾
        int way[1000];//记录后继 way[i] 表示i的后继
        int k = 0;//记录后继
        //动态方程
        for (int i = n - 1; i >= 1; i--) {
            //找谁和i相连
            max = 0;//先初始化max
            k = 0;
            for (int j = i + 1; j <= n; j++) {
                //如果i 和 j 相连 尝试更新m[i]
                if (isAccess[i][j] && m[j] > max) {
                    max = m[j];
                    k = j;
                }
            }
            m[i] = max + boomNum[i]; 
            way[i] = k;//更新后继 记录路径
    
        }
    
        max = -1;
        int max_i;
        //找出最大
        for (int i = 1; i < n; i++) {
            //cout << way[i];
            if (m[i] > max) {
                max = m[i];
                max_i = i;
            }
        }
    
        //输出路径
        cout << max_i;
    
        int t = max_i;
        while (way[t] > 0) {
            cout << "-" << way[t];
            t = way[t];
        }
        cout << endl;
        cout << max;
    }

    这题突破口在题目设置 “并规定路径都是单向的,且保证都是小序号地窖指向大序号地窖” ,那么最大的那个地窖不指向任何地窖,则可以固定最大号的那个地窖的d,从大往小递推。

    设d[i]为从i号地窖开始挖最大的地雷数,默认值为a[i]

    令a[i] 为第i号地窖的地雷数

    递归方程 : d[i] = max { d[j] + a[i] | i 与 j 之间相连} (i : 从大到小)

    设way[i] 为i号地窖的后继地窖,默认为-1

    way[i] = { j | i 与 j 相连 并且 d[j] + a[i] 取得最大 }

    分割线----------------------------------------------------------------------------------------------------

    合唱队形问题

    题目描述
    N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学不交换位置就能排成合唱队形。

    合唱队形定义:设K位同学从左到右依次编号为1, 2, …, K,他们的身高分别为T1, T2, …, TK,

    则他们的身高满足T1 < T2 < … < Ti, Ti > Ti+1 > … > TK (1 <= i <= K)。
    要求:已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

    输入
    输入的第一行是一个整数N,表示同学的总数。
    第一行有n个整数,用空格分隔,第i个整数Ti是第i位同学的身高(厘米)。

    输出
    输出包括一行,这一行只包含一个整数,就是最少需要几位同学出列。

    分析:这一题是最长单调递增子序列的拓展题,

    设有n个人,数列为a

    设d1[i]为从第一个开始到第i个人的最长单调递增子序列的大小,d2[i]为从第i歌人到最后一个的最长单调递减子序列的大小,d1,d2默认值大小为1,

    那么合唱队形最长大小为 : longest = max( 1<=i <= n){ d1[i] + d2[i] - 1}

    出列人数为 ans = n - longest 

    递归方程 :

      d1[i] = max(1 <= j < = i-1 ) { d[j] + 1 | a[i] > a[j] }

      d2[i] = max(从n到i+1) { d[j] + 1 | a[i] < a[j] }

    分割线----------------------------------------------------------------------------------------------------

    描述
    政府在某山区修建了一条道路,恰好穿越总共m个村庄的每个村庄一次,没有回路或交叉,任意两个村庄只能通过这条路来往。已知任意两个相邻的村庄之间的距离为di(为正整数),其中,0 < i < m。为了提高山区的文化素质,政府又决定从m个村中选择n个村建小学(设 0 < n < = m < 500 )。请根据给定的m、n以及所有相邻村庄的距离,选择在哪些村庄建小学,才使得所有村到最近小学的距离总和最小,计算最小值。

    输入
    第1行为m和n,其间用空格间隔
    第2行为(m-1) 个整数,依次表示从一端到另一端的相邻村庄的距离,整数之间以空格间隔。

    例如
    10 3
    2 4 6 5 2 4 3 1 3
    表示在10个村庄建3所学校。第1个村庄与第2个村庄距离为2,第2个村庄与第3个村庄距离为4,第3个村庄与第4个村庄距离为6,...,第9个村庄到第10个村庄的距离为3。
    输出
    各村庄到最近学校的距离之和的最小值。
    样例输入
    10 2
    3 1 3 1 1 1 1 1 3
    样例输出
    18

    分析:这一题有一点难度,一时半会可能是看不懂的,问题不大,反正都看不懂,我懂了就行,2333,希望大家也能看懂。

    设:

      f[i][j]=从第1个到第i个村庄里修j个学校的最短距离和,先把所有f[i][j]初始成一个较大的数如999999

      s[i][j]:从第i个村到第j个村修一个学校,区内村庄到学校的距离和的最小值。(这个学校建在(i+j)/2的地方是最近的,数学证明就不证了,因为我也不会)

      a[i]:第i个村离起点(第一个村)的距离。(用来计算s[i][j]用的,  s[i][j] = ∑(k从i到j) abs(a[k] - a[mid]))

    递归方程:

      f[i][j]= min (f[i][j] , f[k][j-1] + s[k + 1][i]) (k的范围是"已有学校数"~m-1),这个容易看不懂,没关系具体化一下就好懂了

    令i=10, j=1,那么当前f[10][1] = min (f[10][1] , f[k][0] + s[k + 1][10]),现在就是将 【从第1个到第10个村庄里修1个学校的最短距离和】,与 min {【从第1个到第k个村庄里修个0学校(就是不修这个学校,把这个学校放到k+1到10这一段中),从第1个村到第10个村中修一个学校(k从0(已有学校数)到9,),区内村庄到学校的距离和的最小值。】}进行比较,取较小的那个作为f[i][j]。其他情况 类推就好

    考虑初始值设置: 

      f[1][1] f[2][2] = 0,前i个村里修i个学校的最短距离和一定为0 所以 f[i][i] = 0

      f[2][1] f[3][1] = s[1][i] ,前i个村里修1个学校的最短距离和等于s[1][i](从第1个村到第i个村修一个学校,区内村庄到学校的距离和的最小值。)

     

    递归方程

    for(i=2;i<=m;i++){//村庄
            for(j=2;j<=min(i,n);j++){//学校 
                for(int k=j-1;k<=i-1;k++){//枚举已有的学校管辖的范围 
                    if(i!=j)f[i][j]=min(f[i][j],f[k][j-1]+s[k+1][i]);
                }    
            } 
        }
        

    分割--------------------------------------线线

    剑客决斗,(这题有点难度)

    描述
    在路易十三和红衣主教黎塞留当权的时代,发生了一场决斗。n个人站成一个圈,依次抽签。抽中的人和他右边的人决斗,负者出圈。这场决斗的最终结果关键取决于决斗的顺序。现书籍任意两决斗中谁能胜出的信息,但“A赢了B”这种关系没有传递性。例如,A比B强,B比C强,C比A强。如果A和B先决斗,C最终会赢,但如果B和C决斗在先,则最后A会赢。显然,他们三人中的第一场决斗直接影响最终结果。

    假设现在n个人围成一个圈,按顺序编上编号1~n。一共进行n-1场决斗。第一场,其中一人(设i号)和他右边的人(即i+1号,若i=n,其右边人则为1号)。负者被淘汰出圈外,由他旁边的人补上他的位置。已知n个人之间的强弱关系(即任意两个人之间输赢关系)。如果存在一种抽签方式使第k个人可能胜出,则我们说第k人有可能胜出,我们的任务是根据n个人的强弱关系,判断可能胜出的人数。

    输入
    第一行是一个整数N(1<=N<=20)表示测试数据的组数。
    第二行是一个整数n表示决斗的总人数。(2<=n<=500)
    随后的n行是一个n行n列的矩阵,矩阵中的第i行第j列如果为1表示第i个人与第j个人决斗时第i个人会胜出,为0则表示第i个人与第j个人决斗时第i个人会失败。
    输出
    对于每组测试数据,输出可能胜出的人数,每组输出占一行
    样例输入
    1
    3
    0 1 0
    0 0 1
    1 0 0
    样例输出
    3

    思路:只要本人能和本人决斗,则说明此人胜出

    编号为i的人能从所有人中胜出,必要条件是他能与自己相遇,即把环看成链,i点拆成两个在这条链的两端(i开头,j结尾,j也是自己的话那么i胜出),中间的人全部被淘汰出局,i保持不败。这样,在连续几个人的链中,只须考虑头尾两个人能否胜利会师,中间的则不予考虑,从而少了一维状态表示量。

    设:

      meet[i][j]记录i和j能否相遇,能相遇则为true,否则为false。默认为false,初始值meet[i,(i+1) % n] = true。

      fight[i][j]为i和j打,true为i胜出,false为j胜出,(这里fight就是输入样例里的矩阵,1为true,0为false)

    问题转化成了是否能在这条链中找到一个k,使得 i 和 k , k 和 j 均能相遇,且 i 或者 j 能打败 k ,这样最终使得i,j可以相遇

    if(meet[i][k] && meet[k][j]) && (fight[i][k] || fight[j][k]=true) && i < k < j)
         meet[i][j] = true;
    else
         meet[i][j] = false;

    初始值meet[i,i+1] = true,计算顺序是沿对角线的顺序。


    最后计算meet[i][i] = true的数量 (i从1到n)

    这题难点2在于如何填这个表,我看一下别人的博客也是不明不白的,首先是肯定有i,j,k的三重循环,k是从i+1到 j-1这点都没有疑问。问题在于他们为什么设了一个int d = 2,画了画图发现是因为是填表是沿对角线的顺序的,假设n=4,那么初始值meet[0][1](meet[i][j]后面简称m[i][j]), m[1][2] ,m[2][3],m[3][0] 为true,第一次填表的地方在m[0][2],因为m[0][1]和m[1][2]为true,再根据fight表就能确定m[0][2]填什么,第二次填表位置是m[1][3],是由m[1][2]和m[2][3]和他们对应的fight决定,所以计算顺序是沿对角线的顺序,后面依次类推。

    所以有

        for(int d=2;d<=n;d++)
                for(int i=0;i<n;i++)
                {
                    int j = i+d;
                    for(int k=i+1;k<j;k++)
                    {
                        if(meet[i][k%n] && meet[k%n][j%n] && (beat[i][k%n] || beat[j%n][k%n]))
                        {
                            meet[i][j%n] = true;
                            break;
                        }
                    }
                }
          

    最后计算meet[i][i] = true的个数为答案。

    分割----------------------------------------------线

    石子合并问题,(不能用贪心算,容易踩坑)
    有 n 堆石子堆放在路边,现要将石子有序地合并成一堆,规定每次只能移动相邻的两堆石子合并,合并花费为新合成的一堆石子的数量。求将这 N 堆石子合并成一堆的总花费(最小或最大)。

    Min [i][j] 代表从第 i 堆石子到第 j 堆石子合并的最小花费,初始化 i=j时 min[i][j] = 0

    w ( i , j )代表从 i 堆到 j 堆的石子数量之和。

    列出递归式:
      Min [ i ][ j ] = min ( Min [ i ][ k ] + Min [ k + 1][ j ] + w ( i , j )) , i < j( i ≤ k < j)

      重点在:列出如何 Min [ i ][ k ] + Min [ k + 1][ j ] + w ( i , j ) , 若只有两堆,合并花费为两堆个数之和(min[1][1]=0 + w(1,2)),若有三堆,合并 1/2合并花费 (见前分析)+ (1/2)/3合并花费,其中(1/2)/3合并花费为w(1,3),若有四堆合并(1/2),合并(3/4),合并(1/2)/(3/4)花费为w(1,4),总之要加上这个w(i,j)

      Max把min改成max就好

    填表方式:沿正对角线

    -----------------------------------------------分割

    加分二叉树

    设一个n个节点的二叉树tree的中序遍历为(1,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第i个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
    subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数
    若某个子树为主,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空
    子树。
    试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;
    (1)tree的最高加分
    (2)tree的前序遍历

    Input
    第1行:一个整数n(n<30),为节点个数。
    第2行:n个用空格隔开的整数,为每个节点的分数(分数<100)。

    Output
    第1行:一个整数,为最高加分(结果不会超过4,000,000,000)。
    第2行:n个用空格隔开的整数,为该树的前序遍历。

    Sample Input
    5
    5 7 1 2 10

    Sample Output
    145
    3 1 2 4 5

    分析:首先中序遍历有个特点,在中序遍历这个序列上,某个点左边的序列一定是这个点的左子树,右边的序列,一定在这个点的右子树。先搞清楚左右子树的概念

    设:

      root[i][j]表示[i,j]这段序列最大加分的根结点

      dp[i][j]为从i到j节点的加分最大值 , 默认先都设为0,后面有初始化
      value[i]为结点i的分数

    我们可以枚举[i,j]之间的一个点k为根节点,得到状态转移方程 :

      dp[i][j] = max(dp[i][j] , dp[i][k-1] * dp[k+1][j]+ value[k])同时更新root[i][j],最后的结果就是dp[1][n]

    初始化:

      dp[i][i-1] = 1,初始化成1,就不用考虑k没有左或者右子树的情况了

      dp[i][i] = value[i],

      root[i][i] = i,

    填表顺序 自低向上,从左往右,k从i到j

      

    for (int i=n;i>=1;i--)
        {
            for (int j=i+1;j<=n;j++)
            {
                for (int k=i;k<=j;k++)
                    if (dp[i][k-1] * dp[k+1][j] + value[k]> dp[i][j])
                    {
                        dp[i][j] = dp[i][k-1] * dp[k+1][j] + value[k];
                        root[i][j]=k;
                      
                    }
            }
        }

    递归输出前序遍历

    void Outp(int l,int r)
    {
        if (l>r)
            return;
        cout<<root[l][r]<<' '; //前序遍历,所以先输出根节点
        if (l==r)
            return;
        Outp(l,root[l][r]-1);//递归调用
        Outp(root[l][r]+1,r);
        return;
    }

    ---------------------------------------------------

    题目描述 Description

    给出一个长度不超过200的由小写英文字母组成的字母串(约定;该字串以每行20个字母的方式输入,且保证每行一 定为20个)。要求将此字母串分成k份(1<k<=40),且每份中包含的单词个数加起来总数最大(每份中包含的单词可以部分重叠。当选用一 个单词之后,其第一个字母不能再用。例如字符串this中可包含this和is,选用this之后就不能包含th)(管理员注:这里的不能再用指的是位 置,不是字母本身。比如thisis可以算做包含2个is)。
    单词在给出的一个不超过6个单词的字典中。
    要求输出最大的个数。

    输入描述 Input Description

    第一行为一个正整数(0<n<=5)表示有n组测试数据
    每组的第一行有二个正整数(p,k)
    p表示字串的行数;
    k表示分为k个部分。
    接下来的p行,每行均有20个字符。
    再接下来有一个正整数s,表示字典中单词个数。(1<=s<=6)
    接下来的s行,每行均有一个单词。

    输出描述 Output Description

    每行一个整数,分别对应每组测试数据的相应结果。

     

    样例输入 Sample Input

    1
    1 3
    thisisabookyouareaoh
    4
    is
    a
    ok
    sab

    样例输出 Sample Output

    7

     
    分析:(转移方程有点类似建学校,但还是有差别,难度稍大些)

    设:

      dp[i][k]表示将前i个字符划分成k段时的最优解(与乘积最大类似),dp初始化:全0, dp[i][1] = a[1][i]

      a[i][j]为区间i到j包含的单词个数加起来的总和,注意这里有一个部分重叠的条件(包含的单词可以部分重叠。当选用一 个单词之后,其第一个字母不能再用。)

    转移方程有:

      dp[i][k] = max{dp[ j ][ k - 1 ]+ a[ j + 1 ][ i ] }( j从k-1到i-1 

     

    这样就需要预处理每一个区间[i,j]中的单词个数.也就是a[i][j]

     

    对于区间[i,j],如果以str[i]开头有单词匹配,那么a[i][j]=a[i+1][j]+1.  否则a[i][j]=a[i+1][j].

    void pre(){
        memset(a,0,sizeof a);
        for(int i=1;i<=len;i++){
            if(match(i,i)) a[i][i]=1;
            for(int j=i-1;j>=1;j--){
                if(match(j,i)) a[j][i]=a[j+1][i]+1;
                else a[j][i]=a[j+1][i];
            }
        }
    }

    执行步骤 : i = 1  // i = 2 j =1 // i = 3 j = 2,1 // i = 4 j = 3,2,1

    具体说一下这个函数,举个例子,比如 i = 3 j = 2,1 ,先match(2,3)匹配到了就是a[2][3] = a[3][3] + 1否则等于  a[3][3],接着再match(1,3) 匹配到了就是a[1][3] = a[2][3] + 1否则等于 a[2][3],这样就回应了题目当选用一 个单词之后,其第一个字母不能再用的要求,所以是从n往前这样匹配来累加。

     

    转移方程:

    for(int k=2;k<=K;k++)
            for(int i=k;i<=len;i++)
                for(int j=k-1;j<i;j++)
                    dp[i][k]=max(dp[i][k],dp[j][k-1]+a[j+1][i]);

    k从2到K段,i从k到到字符串长度。

  • 相关阅读:
    (转)Python中的__init__和__new__
    PEP8
    python lstrip()函数
    python中的生成器跟迭代器
    callback
    关于0.0.0.0这个ip的疑问
    Python import中相对路径的问题
    python读取excel
    git本地管理多个密钥/账户
    词法分析之有确定、不确定自动机及其生成器
  • 原文地址:https://www.cnblogs.com/likeghee/p/11748044.html
Copyright © 2011-2022 走看看