其他分享
首页 > 其他分享> > 区间DP の 题(内含 最长回文串,石子合并,删除字符串,乘积最大,释放囚犯)

区间DP の 题(内含 最长回文串,石子合并,删除字符串,乘积最大,释放囚犯)

作者:互联网

乘积最大

  由于题目给定的是m,需要分解成m+1部分的乘积,不难想到乘号刚好是m个,那么该题就转化成了m个乘号的插入方式。 

 最优子结构分析:     

 设数字字符串为a1a2…an        

   m=1 时,一个乘号可以插在a1a2…an中的n-1个位置,这样就得到n-1种乘积:                        

     a1*a2…an,   a1a2*a3…an, …, a1a2…a n-1*an           

    此时的最大值= max { a1*a2…an, a1a2*a3…an, …, a1a2…a n-1*an }        

   m=2时,两个乘号可以插在a1a2…an中n-1个位置的任两个地方,这样总共会产生 

  个乘积。把这些乘积分个类,便于观察规律。 

Case1: a1a2 …*a n-1*an , a1a2 …*a n-2 a n-1*an , a1*a2 …a n-3 a n-2 a n-1 *  an ,   

  因后一个乘号位置不变,要使这些乘积最大,就要找出在前n-1个数中插入一个乘号的最大值。引入符号F(n-1,1)表示在前n-1个数中插入一个乘号的最大值,则  Case1的最大值为F(n-1, 1) * an

Case2: a1a2 …*a n-2*a n-1 an , a1a2 …*a n-3 a n-2* a n-1 an , a1*a2 …a n-3 a n-2 * a n-1 an ,     

  因后一个乘号位置不变,要使这些乘积最大,就要找出在前n-2个数中插入一个乘号的最大值。设符号F(n-2)(1)为在前n-2个数中插入一个乘号的最大值,则 Case2的最大值为F(n-2,1) * a n-1 an 

同理,Case3的最大值为F(n-3,1) * a n-2 a n-1 an  …… Case n-2的最大值为F(2,1) * a3…an 

把一段数字串转化为一个整数:

int get(int i,int j) { //将i~j这一段数字转化为一个整数 
	int ret=0;
	for(int k=i;k<=j;k++) {
		ret=ret*10+s[k]-'0';
	}
	return ret;
}

状态转移方程推导: 

  下面以n=9, m=4, s=‘321044105’为例,说明计算过程。              

  我们引入符号f(i,j)表示从a1~ai中插入j个乘号取得的最大值,g[i][j]表示从ai~aj的子串的数值。则上式可表示为:    

  F(9,4) = max{f(8,3)*g[9][9], f(7,3)*g[8][9], f(6,3)*g[7][9], f(5,3)*g[6][9], f(4,3)*g[5][9]}    

  F(8,3) = max{f(7,2)*g[8][8], f(6,2)*g[7][8], f(5,2)*g[6][8], f(4,2)*g[5][8],f(3,2)*g[4][8]}    

  F(7,3) = max{f(6,2)*g[7][7], f(5,2)*g[6][7], f(4,2)*g[5][7], f(3,2)*g[4][7]}    

   …………    

  上面的分析已经看出问题的最优子结构、重复子问题性质。 

 定义状态:dp[i][j]表示从a1~aj中插入i个乘号取得的最大值

dp[i][j] = max{ dp(i-1, k)*get(k+1, j) }  (1<=i<=m, i+1<=j<=n, i<=k<j )

  i的取值范围为:乘号个数

  j的取值范围为:乘号数i加1 ~ 数字串总个数

  k的取值范围为:乘号个数~右边界j减去1

  上式的边界条件是什么?

  dp[0][i] = get(1, i)  (1<=i<=n)

  我们要求的问题的最优解是dp[m][n] 

 

Code
 #include <bits/stdc++.h>
using namespace std;
long long n,k,dp[45][45],m;
char a[45];
long long num(long long x,long long y)
{
	long long sum=0;
	for(long long i=x;i<=y;i++)
	{
		sum=sum*10+a[i]-'0';
	}
	return sum;
 } 
int main()
{
	scanf("%lld%lld",&m,&n);
	scanf("%s",a+1);
	for(int i=1;i<=m;i++)
	{
		dp[0][i]=num(1,i);
	}
	for(long long i=1;i<=n;i++)
	{
		for(long long j=i+1;j<=m;j++)
		{
			for(long long k=i;k<j;k++)
			{
				dp[i][j]=max(dp[i-1][k]*num(k+1,j),dp[i][j]);
			}
		}
	}
	printf("%lld\n",dp[n][m]);
}

 

删除字符串

  定义dp[i][j]为删除区间[i,j]的最少次数

  (1)如果s[i]==s[j],dp[i][j] = dp[i+1][j-1] + 1,即先删除区间[i+1,j-1]再把相同的s[i]s[j]一次删除;

  (2)如果s[i] != s[j],dp[i][j] = min(dp[i+1][j],dp[i][j-1]) + 1,只能先删除区间[i+1,j]或者[i,j+1],最后删除区间端点的单个字符;但是如果有aabb这种串的话,上面第二种做法就要删3次,显然不对!

  (3)然后枚举区间[i,j]的分割点k,   dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]-1),这样的话k这个点删了两次,所以要减1。 

 

#include<bits/stdc++.h>
using namespace std;
char c[505];
int main() 
{
	int n, idx, a[505], dp[505][505];
    scanf("%d",&n);
    scanf("%s",c);
    a[0]=c[0];
    for (int i = 0; i < n; i++) 
        if (c[i]!= c[i-1])
            a[++idx] = c[i];
    for(int i=1;i<=idx;i++)
    	dp[i][i]=1;
    for (int len=2;len<=idx; len++) 
	{
        for (int i=1;i<=idx-len+1;i++)
		 {
		 	int j=i+len-1;
            if(a[i]==a[j])
            	dp[i][j]=dp[i+1][j-1]+1;
			else
				dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1;
           	for(int k=i+1;k<=j;k++)
           		dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]-1);
        }
    }
    printf("%d", dp[1][idx]);
    return 0;
}

 

石子合并

   假设只有2堆石子,显然只有1种合并方案  如果有3堆石子,则有2种合并方案,((1,2),3)和(1,(2,3))  如果有k堆石子呢? 

                           

不管怎么合并,总之最后总会归结为2堆,如果我们把最后两堆分开,左边和右边无论怎么合并,都必须满足最优合并方案,整个问题才能得到最优解。

 令m[i,j]表示归并第i个数到第j数的最小代价,w[i,j]表示第i个数到第j个数的和,这个可以事先计算出来。有如下的状态转移方程: 

                 

预处理w[i][j]:  

  先算出前缀和s[i],然后w[i][j] = s[j] – s[i-1]  

因此,w[i][j]可以不事先存放,在DP时直接O

  (1)计算   s[0] = 0;   for(int i = 1; i<=n; i++) s[i] = s[i-1]+a[i]; 

拓展成环:  

  对于环状序列,通用的处理方法是把这条链延长2倍,扩展成2n-1堆,其中第1堆与n+1堆完全相同,第i堆与n+i堆完全相同,这样我们只要对这2n堆动态规划后,枚举f(1,n),f(2,n+1),…,f(n,2n-1)取最优值即可即可。 

                         

 

Code
 #include<bits/stdc++.h>
using namespace std;
int dp[305][305],len,a[305],n,sum[305];
int main()
{
	cin>>n;
	memset(dp,0x3f,sizeof(dp));
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
		dp[i][i]=0;
	}
	for(int len=2;len<=n;len++)
	{
		for(int i=1;i<=n-len+1;i++)
		{
			int j=i+len-1;
			for(int 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];
}

 

最长回文串

  令dp[i][j]表示在字符串区间s[i]..s[j]之间的最长回文串的长度。  

  分几种情况讨论:

(1)如果s[i] = s[j],这时如果s[i+1]..s[j-1]是回文串,则加上两端的回文,可构成一个更长的回文串。怎以判断s[i+1]..s[j-1]是不是回文串呢?   一个非常简单的办法是:dp[i+1][j-1]是否等于j-i-1 

                         

  如果s[i+1]..s[j-1]不构成回文串,则dp[i][j]在dp[i+1][j]与dp[i][j-1]中取较大值。

(2)如果s[i]!=s[j],则dp[i][j]在dp[i+1][j]与dp[i][j-1]中取较大值者这仍然是一个区间DP的模型。 

Code
 #include<bits/stdc++.h>
using namespace std;
int dp[3005][3005];
int main()
{
	char s[3005];
	scanf("%s",s+1);
	int n=strlen(s+1);
	for(int i=1;i<=n;i++)
	{
		dp[i][i]=1;
	}
	for(int len=2;len<=n;len++)
	{
		for(int i=1;i<=n-len+1;i++)
		{
			int j=i+len-1;
			if(dp[i+1][j-1]==j-i-1&&s[i]==s[j])
			{
				dp[i][j]=dp[i+1][j-1]+2; 
			}
			else
			{
				dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
			}
		}
	}
	printf("%d",dp[1][n]);
	return 0;
}

 

释放囚犯

  区间dp的套路:设f[i][j]为区间释放i~j号囚犯所需最少的肉(注意,i,j不是牢房编号,是释放的囚犯编号,也就是下面的a[i]数组)

  枚举区间的分界点k,转移方程为:

  f[i][j]=min{f[i][j],f[i][k-1]+f[k+1][j]+a[j+1]-a[i-1]-1-1}

  把后面这一坨拿出来拆开看看,

  f[i][k-1]+f[k+1][j],这个不必解释

  a[j+1]-a[i-1]-1就是第j+1个要放出的囚犯到第i-1个要放出的囚犯之间的人数,也就是要发的肉的数量;

  最后一个-1 是什么呢,就是第k个放出去的囚犯,不用给他吃肉了

  注意一件事:输入的囚犯的编号。当你细细的观察它们时,你会发现第 Qi​ 个囚犯的编号Qi​ 等于他及其前面的所有人的人数,那么这就相当于是一个前缀和,又因为我们放第 Qq 个囚犯的时候需要给最后一段人肉,所以我们可以假设在这段监狱的最后(p+1)还有一个需要释放的囚犯。

  再假设我们此时释放囚犯 k,那么我们此时需要的肉的数量即为释放第 Qi​ 个囚犯到第 Qk−1​ 个囚犯与释放第 Qk+1​ 个囚犯到第 Qj​ 个囚犯所需的总肉数加上施放这个囚犯所需的肉的数量。由于我们先选择释放第Qk​ 个囚犯,所以我们需要用 a[j+1]-a[i-1]-2 的肉(我们的假设是除了第 Qi​ 个囚犯到第 Qj​ 个囚犯未释放外其他囚犯均已释放,只不过没用肉。),由于先放哪一个囚犯最优不清楚,于是取最小值。

#include<bits/stdc++.h> 
using namespace std;
int a[105];
int dp[105][105];
int main()
{
    int p,q;
    scanf("%d%d",&p,&q);
    for(int i=1;i<=q;i++)
        scanf("%d",&a[i]);
    a[0]=0;
    a[q+1]=p+1;
    sort(a+1,a+q+1);
    for(int len=1;len<=q;len++)
    {
        for(int i=1;i+len-1<=q;i++)
        {
            int j=i+len-1;
            dp[i][j]=0x3f3f3f3f;
            for(int k=i;k<=j;k++)
                dp[i][j]=min(dp[i][j],dp[i][k-1]+dp[k+1][j]+a[j+1]-a[i-1]-2);
        }
    }
    printf("%d",dp[1][q]);
    return 0;
}

 

标签:囚犯,乘积,int,最大值,long,a1a2,DP,dp,回文
来源: https://www.cnblogs.com/pangtuan666/p/16598200.html