动态规划三:区间动态规划(19182+11078+洛谷P4170+力扣877)

  (一)区间动态规划简介

        用算法解决问题时经常采用“化简”的方式,将一个大问题拆分成若干个独立的子问题。如果一个问题最终被分解成为两个连续的部分(一个序列被分割为两个子段),然后再合并,那么几乎可以确定是区间规划问题。当然,分析过程才是算法的难点。

       区间动态规划用于求区间上的最优值,对于整个区间的最优值来说,通过枚举区间左右两部分的分割点(合并点),将问题分解成为左右两部分的子问题,最后将左右两个子问题的最优值进行合并计算得到原问题的最优值。

 (二)区间动规的算法实现(OJ19182)

        石子合并是区间动态规划的模板题。

设有 N(N≤300) 堆石子排成一排,其编号为1,2,3,⋯,N。每堆石子有一定的质量 mi(mi≤1000)。
现在要将这N堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。
合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。

        发挥递归的思想(还记得文章“动态规划一”中介绍的动规算法四步分析吗?)。N个石子归并代价如果定义为f(1,N),那么在最后一次合并前,一定已经合并为2堆,这两堆的分割当然有很多种可能性。比如左边1个石子单独一堆,右边N-1个一堆。那么问题就转换为f(1,N)=f(1,1)+f(2,n)。当然,也可能是f(1,N)=f(1,2)+f(3,n),或者f(1,N)=f(1,3)+f(4,n)...........

         这样我们就得到解决问题的递归算法公式:f(1,N) = min(f(1,i)+f(i+1,N)+sum(1,N)) (1<=i<N)    sum函数计算1至N石子的和(合并代价)。

        递归属于逆向思考,用正向方式定义dp[i][j]为归并区间[i,j]的最小代价,那么状态转移方程:dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum[i,j]) (i<=k<j)  

题目的最优解为dp[1][N],如果求石子合并最大代价同理。

#include <iostream>  #include<cstring>
using namespace std;
int n,a[305],sum[305],dp[305][305]= {0}; /**< dp[i][j]是区间[i,j]的最小代价 */
int main()
{  ios::sync_with_stdio(0),cin.tie(0);
    int i,j,k,m,n,p;
    cin>>n;
    for(i=1; i<=n; i++) /**< sum为前缀和数组 */
        cin>>a[i],sum[i]=sum[i-1]+a[i];
    for(p=2; p<=n; p++)/**< p枚举区间长度 */
    {
        for(i=1; i<=n-k+1; i++)
        { /**< 求dp[i][j]最小值 */
            j=i+p-1; /**< 区间[i,j]长度为p */
            dp[i][j]=1e9;/**< 初始化大值 */
            for(k=i; k<j; k++)
                dp[i][j]= min(dp[i][j], dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
        }
    }
    cout<<dp[1][n];
    return 0;}

 (三)区间动规的一些经典例题

(1)11078 不能移动的石子合并

有n堆石子A1,A2,...,An形成首位相连的一个环形,An和A1相邻,每堆石头个数记为ai(1<=i<=n),相邻两堆可合并,合并的分值为新堆的石子数。求合并为一堆的最低得分和最高得分。

        关于环形数据的处理,常用的作法是双倍存储,也就是说如果原数据存储在a[1].....a[n],那么让a[n+1]=a[1],a[n+2]=a[2].......a[2*n]=a[n]。这样环的n-1种拆分(把环变成线性)都可以在这个2n的数组中体现出来。如果在1和n进行拆分,那么问题就和传统的石子合并一样,即dp[1][n],如果在1和2之间拆分,那么答案就是dp[2][n+1],同理,枚举2n数组中所有长度为n的区间的最优值,必然能找到环的最优解。

        由于OJ11078代码较长,此处用洛谷P1063 能量项链的代码进行演示:

#include <stdio.h>  
#include <algorithm>  
using namespace std;  
int a[205],dp[205][205];  
int main()  
{  
    int n,ans = 0;;  
    scanf("%d",&n);  
    for(int i = 1;i <= n;i++){  
        scanf("%d",a+i);  
        a[i+n] = a[i];  /**< 双倍造环 */
    }  
    for(int p = 2;p <= n;p++){  
        for(int i = 1,j = p;i <= 2*n-1 && j <= 2*n+1;i++,j++){ /**< dp[i][j]为长度p的区间 */
            for(int k = i;k < j;k++){  
                dp[i][j] = max(dp[i][j],dp[i][k]+dp[k+1][j]+a[i]*a[k+1]*a[j+1]);  
            }  
        }  
    }  
    for(int i = 1;i <= n;i++) /**< 枚举所有长度为n的区间 */ 
        ans = max(ans,dp[i][n+i-1]);  
    printf("%d\n",ans);  
    return 0;  }  

(2)洛谷P4170涂色

   

         分析一下,按题目给定的涂色方式,如果想涂色次数尽可能的少,只能采用先外后内的方式,例如颜色RGGR,显然要先涂区间[1,4]为R,然后[2,3]为G。如果是RGRGR,可以先涂[1,5]R,然后[2,4]G,然后[3,3]R。另一种涂法是[1,5]R,[2,2]G,[4,4]G。最少需要3次涂色。

        题目并没有明确表现出两两合并的特性,但进一步分析如果想要完成区间[i,j]的涂色,那么就只有两类可能,(1)a[i]==a[j],此时可以先处理区间[i,j-1],因为涂色是涂两端,在涂a[i]时可以顺路涂好a[j],当然也可能是先处理[i+1,j]。(2)a[i]!=a[j],因此无法用一次涂色处理a[i]和a[j]两个点,那么可以先对a[i]涂色,然后对[i+1,j]涂色,或者是对a[j]涂色,然后对[i,j-1]涂色。问题是如果用这种分析法样例中AABB答案会是4。实际上应该设置断点,此时问题转换为明显区间合并问题。a[i]和a[j]不一样,他们涂色一定是独立的,那么可以先对[i,k]涂色,再对[k+1,j]涂色。枚举这个k,答案可得。

#include <bits/stdc++.h>
using namespace std;
int main()
{
    int i,j,k,n,p,dp[105][105];
    char a[105];
    memset(dp,127/3,sizeof dp);/**< dp数组初始化大值 */
    a[0]='\0';
        scanf("%s",a+1);
    n=strlen(a+1);
    for(i=1;i<=n;i++)/**< 区间长度1的值特殊处理 */
        dp[i][i]=1;
    for(p=2;p<=n;p++)
    {
        for(i=1,j=i+p-1;j<=n;i++,j++)
        {
            if(a[i]==a[j])/**< 可以在涂i或j时不消耗涂色次数 */
                dp[i][j]=min(dp[i+1][j],dp[i][j-1]);
            else
            {
                for(k=i;k<j;k++)/**< 枚举分割点 */
                dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]);
            }
        }
    }
    cout<<dp[1][n];
    return 0;}

(3)力扣877 石子游戏

leetcode的题目就不提供代码了,仅做分析。

博弈论的问题同样可以用递归思想去考虑每一步选择后的变化。本题目由于只能拿两端石子,那么A玩家拿走左边第一个石子时,问题就变成[2,N]的问题,B玩家是先手,A玩家变成后手。如果设定f(1,N)为先手玩家的最优值,那么只有两种可能:

第一种:f(1,N)= a[1]  +( sum[2,N]-f(2,N))  A先手拿了a[1]石子,那么后手的B对于剩下的[2,N]区间来说是先手,B拿剩下的石子属于A玩家。

第二种:(1,N)= a[N]  +( sum[1,N-1]-f(1,N-1))    A先手拿了a[N]石子,那么后手的B对于剩下的[1,N-1]区间来说是先手,同样B拿剩下的属于A玩家。

如此一来问题仍然是区间最大值问题,此题目先手不一定必胜。

dp[i][j]的公式本文不再给出,由读者们自行推导。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值