全网最最最最最详细的区间dp题目总结!!

QWQ最近在做一些区间dp的题,有感而发 嗯


如果在阅读本文时遇到什么问题或者解法本身有什么漏洞,可以随时联系我 +q  752742355


区间dp,顾名思义,就是解决一些区间内最优值的问题,通常的时间复杂度为n^2 或者 n^3


而区间dp的大致思路就是



首先确定状态

初始化长度为1(or 2,3....具体因题而异)的dp数组的值

然后枚举区间长度,枚举区间的起始点,(有的题目还需要枚举断点) 由小区间转移到大区间。

最后dp[1][n]往往就是答案。



首先,我们先来看一道例题:


NOI1995 石子合并

在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出1个算法,计算出将N堆石子合并成1堆的最小得分。


喵喵喵QWQ第一眼看到这个题目,肯定很多人会想“每次合并代价最小的两堆 难道不对吗?”

8 4 6 3 5

按照贪心 是62  但最优解是60.......


好的,那我们还是开始想dp吧。区间dp常用的一个状态就是dp[i][j]表示i~j这个区间的最优值是多少?


emmmm 我们可以看出题目中这个合并过程,很像是区间dp的一种合并,也就是说dp[i][j]可以由dp[i][k]和dp[k+1][j]转移过来

那么我们自然而然的想到要枚举上一次合并是在哪个位置,也就是断点k,然后进行转移


不过,由于这个题目的特殊限制,我们需要记录g[i][j]表示i到j这个区间有多少石子,来进行辅助转移


好啦!至此,我们的dp方程也就解决了

f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+g[i][k]+g[k+1][j]) ( i<=k<=j)

对于环,我们只需要把整个数组复制一遍,然后在统计答案的时候,把1-n开头的长度为n的区间的答案求一个min 就可以啦

上代码:

#include<bits/stdc++.h>

using namespace std;

const int maxn = 1010;

int g[maxn][maxn],f[maxn][maxn],i,j,k,m,a[maxn],n,g1[maxn][maxn],f1[maxn][maxn];

int ans = 0xfffffff;
int main ()
{
	scanf("%d",&n);
    for (i=1;i<=2*n;i++)
      for (j=1;j<=2*n;j++)
        f[i][j]=0xfffffff;
	for (i=1;i<=n;i++)
	  scanf("%d",&a[i]);	
	for (i=n+1;i<=2*n;i++)
	  a[i]=a[i-n];
	for (i=1;i<=2*n;i++)
	{
	  f[i][i]=0;
	  g[i][i]=a[i];
    }
    for (i=1;i<=n;i++)
      for (j=1;j<=2*n-i;j++)
      {
        for (k=j;k<j+i;k++)
        {
        	int cnt=g[j][k]+g[k+1][j+i];
        	if (f[j][j+i]>cnt+f[j][k]+f[k+1][j+i])
        	{
        	  f[j][j+i]=cnt+f[j][k]+f[k+1][j+i];
        	  g[j][i+j]=cnt;
            }
        }
      }
    for (i=1;i<=n+1;i++)
      ans=min(ans,f[i][i+n-1]);
    cout<<ans<<endl;
    return 0;
}

QWQ早期码风比较清奇 嗯 (竟然不定义l和r 稍微意淫一下)


不过,这道题在hdu上(hdu3506)n^3的时间复杂度过不了


那么我们就需要考虑,四边形不等式优化!!


这里引用一个dalao的博客!      https://blog.csdn.net/noiau/article/details/72514812    

如果有f[a][c]+f[b][d]<=f[b][c]+f[a][d]

可以理解为一句话,交叉小于包含,即交叉的两个区间,a到c和b到d的值满足小于等于包含的两个区间[bc包含于ad]) 

则说这个东西满足四边形不等式,当然这个东西可能是dp数组,也可以是其他数组


这里有两个定理

1、如果上述的w函数同时满足区间包含单调性和四边形不等式性质,那么函数dp也满足四边形不等式性质 

我们再定义s(i,j)表示 dp(i,j) 取得最优值时对应的下标(即 i≤k≤j 时,k 处的 dp 值最大,则 s(i,j)=k此时有如下定理 

2、假如dp(i,j)满足四边形不等式,那么s(i,j)单调,即 s(i,j)≤s(i,j+1)≤s(i+1,j+1) 


那么我们就可以开一个s数组 ,s[i][j]这个数组记录i~j这个区间的转移的最优点是哪个点

那么枚举断点是时候 直接可以从s[i][j-1] ~ s[i+1[j]枚举转移


上代码(hdu3506

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;

const int maxn = 2010;

inline int read()
{
   int x=0,f=1;char ch=getchar();
   while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
   while (isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
   return x*f;
}

int g[maxn][maxn],f[maxn][maxn],i,j,k,m,a[maxn],n;
int s[maxn][maxn];
int ans = 1e9;
int main ()
{
    while (scanf("%d",&n)!=EOF)
    {
    for (int i=1;i<=2*n;++i)
      for (int j=1;j<=2*n;++j)
        f[i][j]=1e9;
    ans=1e9;
    memset(s,0,sizeof(s));
    for (i=1;i<=n;++i)
      scanf("%d",&a[i]);    
    for (i=n+1;i<=2*n;++i)
      a[i]=a[i-n];
    for (i=1;i<=2*n;++i)
    {
      f[i][i]=0;
      g[i][i]=a[i];
      s[i][i]=i;
    }
    for (i=1;i<=n;++i)
      for (j=1;j<=2*n-i;++j)
      {
        for (k=s[j][i+j-1];k<=s[j+1][j+i];++k)
        {
            int cnt=g[j][k]+g[k+1][j+i];
            if (f[j][j+i]>cnt+f[j][k]+f[k+1][j+i])
            {
              f[j][j+i]=cnt+f[j][k]+f[k+1][j+i];
              g[j][i+j]=cnt;
              s[j][j+i]=k;
            }
        }
      }
    for (i=1;i<=n+1;++i)
      ans=min(ans,f[i][i+n-1]);
    printf("%d\n",ans);
    }
    return 0;
}

嗯 那么大概就是这样了

下面的题目以思路为主 不做详细的解释了:


hdu4632 **

大致题意是给定一个字符串,求回文子序列个数,最后的答案要%10007,要求一个n^2的做法

首先定义f数组f[l][r]表示l~r区间的回文子序列个数,f[i][i]=1;

显然 根据容斥原理 :f[l][r]=f[l][r-1]+f[l+1][r]-f[l+1][r-1]  (因为中间的个数会算两遍);

然后,我们考虑s[l]==s[r]的情况,如果这两个位置相等,那么l+1 ~ r-1这个区间的所有子序列,都可以加入l和r这两个元素,构成一个新的回文子序列,除此之外 l和r这两个元素也可以构成一个回文子序列

那么 if  (s[l]==s[r]) f[l][r]+=f[l+1][r-1]+1;


注意!!!!

做减法的时候,可能会有负数,为了使%不出现问题,我们需要先加mod再%mod

上代码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>

using namespace std;

inline int read()
{
   int x=0,f=1;char ch=getchar();
   while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
   while (isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
   return x*f;
}
const int maxn = 2010;
const int mod = 10007;

int f[maxn][maxn];
char s[maxn];
int tmp;
int t;
int n;

int main()
{
  scanf("%d",&t);
  while (t--)
  {
      tmp++;
      scanf("%s",s+1);
      n=strlen(s+1);
      memset(f,0,sizeof(f));
      for (int i=1;i<=n;++i)
        f[i][i]=1;
      for (int i=1;i<n;++i)
        if (s[i]==s[i+1]) f[i][i+1]=3;else f[i][i+1]=2;
      for (int i=3;i<=n;++i)
        {
            for (int l=1;l<=n-i+1;++l)
            {
                int r=l+i-1;
                if (s[l]==s[r])
                  f[l][r]=((long long)f[l+1][r]+f[l][r-1]+1)%mod;
                else
                  f[l][r]=((long long)f[l+1][r]+f[l][r-1]-f[l+1][r-1]+mod)%mod;
            }
        }
      printf("Case %d: %d\n",tmp,f[1][n]%mod);
  }
  return 0;
}

hdu4745*********

题目大意:

给定一个数列,求一个最长的回文子序列


我们定义f[l][r]表示l到r区间的最长的回文子序列长度

如果s[l]==s[r] 则 f[l][r]=f[l+1][r-1]+2;

不然 f[l][r]=max(f[l+1][r],f[l][r-1]);


然后统计答案的时候

因为是环

由于考虑到可以从

同一个点出发,长度为n-1,+1

或者从不同的点出发,长度为n


上代码

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

using namespace std;

inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

const int maxn = 3010;

int a[maxn],f[maxn][maxn];
int n,m;
int ans;

int main()
{ 
  while (1)
  {
  scanf("%d",&n);
  memset(f,0,sizeof(f));
  memset(a,0,sizeof(a));
  if (n==0) return 0;
  ans=0;      
  for (int i=1;i<=n;i++)
    scanf("%d",&a[i]),a[i+n]=a[i];
  for (int i=1;i<=2*n;i++)
    f[i][i]=1;
  for (int i=1;i<=2*n;i++)
    if (a[i]==a[i+1]) f[i][i+1]=2;else f[i][i+1]=1;
  for (int i=2;i<=2*n;i++)
  {
       for (int l=1;l<=2*n-i+1;l++)
       {
           int r=l+i-1;
           if (a[l]==a[r]) f[l][r]=max(f[l][r],f[l+1][r-1]+2);
           else f[l][r]=max(f[l][r-1],f[l+1][r]);
       }
  }
  for (int i=1;i<=n+1;i++)
  {
      ans=max(ans,f[i][i+n-1]);
      ans=max(ans,f[i][i+n-2]+1);
  }
  printf("%d\n",ans);
  }
  return 0;
}


hdu2476******

题目大意:

给定一个A串,一个B串,每次修改只能将一段连续区间内的字母,改成同一个字母,询问将A变成B 的最少操作次数


emmmmmmmmQWQ因为考虑到有相等的和不相等的部分,所以不能直接做


先假设A和B所有的对应位置都不相等,然后令f[l][r]表示l~r这个区间最小操作的次数

然后对一个区间l r 我们可以考虑,首先将f[l][r]=f[l+1][r]+1;表示需要在上一个区间的基础上,额外操作一次。然后枚举一个k

从l+1到r,如果b[k]==b[l] 那么 l这个位置 就可以在刷k的时候 刷上,而且不影响之前的区间。

既 f[l][r]=min(f[l][r],f[l][k]+f[k+1][r])


然后 我们令ans[i]表示A的第1到第i 刷成和B相同的  需要的 最小的操作次数

若a[i]==b[i] 则 ans[i]=ans[i-1] (因为不需要刷)

else 枚举一个j 表示这个ans[i]从哪个答案转移过来

ans[i]=min(ans[i],ans[j]+f[j+1][i]);


上代码

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

using namespace std;

inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

const int maxn  = 110;

int f[maxn][maxn];
char a[maxn],b[maxn];
int lena,lenb;
int n;
int ans[maxn];

int main()
{
  while (scanf("%s",a+1)!=EOF)
  {
  memset(f,0,sizeof(f));
  scanf("%s",b+1);
  n=strlen(a+1);
  for (int i=1;i<=n;i++)
    f[i][i]=1;
  for (int i=2;i<=n;i++)
  {
     for (int l=1;l<=n-i+1;l++)
     {
          int r=l+i-1;
          f[l][r]=f[l+1][r]+1;
          for (int k=l+1;k<=r;k++)
          {
              if (b[k]==b[l])
                f[l][r]=min(f[l][r],f[l+1][k]+f[k+1][r]);
          }
    }
  }
  for (int i=1;i<=n;i++)
      ans[i]=f[1][i];
  for (int i=1;i<=n;i++)
    if (a[i]==b[i])
      ans[i]=ans[i-1];
    else
      for (int j=1;j<i;j++)
        ans[i]=min(ans[i],ans[j]+f[j+1][i]);
    printf("%d\n",ans[n]);
  }
  return 0;
}



hdu4283 ********


题目大意:

现在给定一个序列,表示每个人的不开心值D,假如他是第x个上场,那他就有(x-1)*D的不开心值,因为他等了k-1个人,你有一个类似于栈的东西,可以使用它来改变整个序列的顺序,求最少的 不开心值的和


QWQ由于修改顺序的时候,是要用类似栈的方式QWQ所以没法贪心.....


这个题有一个性质,若第i个人是通过栈调整之后,第k个表演的 那么他能造成的影响 是可以比较容易计算的。


我们令f[l][r]表示 l~r这些人的最小等待时间

首先预处理一下所有人的等待时间的前缀和sum[i],便于计算

对于一个区间l r 我们枚举第l个人是第几个出去的


k从l~r


假设他是第k个出去的

则 f[l][r]=min(f[l][r],f[l+1][k]+f[k+1][r]+(k-l)*a[i]+(k-l+1)*(sum[r]-sum[k]))


这里有几个要注意的地方:

1>是f[l+1][k] 而不是 f[l][k]  因为这里枚举的就是第l个人的出栈顺序

2>(k-l)表示在这个人之前有几个人出去 (k-1+1)表示算上这个人 出去了几个人


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

using namespace std;

inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

const int maxn = 110;

int a[maxn],f[maxn][maxn];
int sum[maxn];
int tmp;
int n,t;

int main()
{
  scanf("%d",&t);
  while (t--)
  {
      tmp++;
    n=read();
      memset(f,0,sizeof(f));
      for (int i=1;i<=n;i++)
        scanf("%d",&a[i]),sum[i]=sum[i-1]+a[i];
      for (int i=1;i<=n;i++)
        for (int j=i+1;j<=n;j++)
          f[i][j]=1e9;
      for (int i=1;i<=n;i++) f[i][i]=0;
      for (int i=2;i<=n;i++)
        for (int l=1;l<=n-i+1;l++)
        {
            int r = l+i-1;
            for (int k=l;k<=r;k++)
            {
                f[l][r]=min(f[l][r],f[l+1][k]+f[k+1][r]+(k-l)*a[l]+(k-l+1)*(sum[r]-sum[k]));
            }
        }
    printf("Case #%d: %d\n",tmp,f[1][n]);
  }
  return 0;
}

poj2955

题目大意:


给定一个只含小括号和中括号的序列,求一个最长的合法序列。

其中() []   ()[]   (()) 等 是合法的

)(  (] 等 是不合法的


由于两个本身合法的序列拼在一起也是合法的序列,所以我们自然而然的就想到

令f[l][r]表示l~r这个区间的最长合法序列的程度

根据a[l] a[r] f[l+1][r-1]进行初始化

然后枚举断点,进行转移


这里有一个小tip就是

可以把左右括号分别变成正负的数字,便于判断

二话不说

直接上代码

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

using namespace std;

inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

const int maxn = 110;

char s[maxn];
int f[maxn][maxn];
int n;
int a[maxn];

int main()
{
	while (1)
	{
  	scanf("%s",s+1);
  	if (s[1]=='e') break;
  	 n=strlen(s+1);
  	 memset(f,0,sizeof(f));
  	 for (int i=1;i<=n;i++)
  	 {
  	    if (s[i]=='(') a[i]=1;
  	    if (s[i]==')') a[i]=-1;
  	    if (s[i]=='[') a[i]=2;
  	    if (s[i]==']') a[i]=-2;
	   }
  	 for (int i=1;i<=n;i++)
  	   for (int l=1;l<=n-i+1;l++)
  	   {
  	   	  int r = l+i-1;
  	   	  if (a[l]+a[r]==0 && a[l]>0) f[l][r]=f[l+1][r-1]+2;
  	   	  else f[l][r]=f[l+1][r-1];
  	   	  for (int k=l;k<=r;k++)
  	   	  {
  	   	  		 f[l][r]=max(f[l][k]+f[k+1][r],f[l][r]);
			}
		 
	  }
	printf("%d\n",f[1][n]);
   }
  return 0;
}


poj1651


题目大意

给定你一个序列

其中取走一个数的代价是它乘上它相邻的两个数

两头的数不可以取~

求最小代价


啊,一开始看这个题的时候,没搞清楚题意。

令f[l][r]表示把l+1 ~ r-1的数都取走的最小代价

首先初始化f[i][i+2]这个区间


然后之后枚举长度,起始点,断点,进行转移即可


f[l][r]=min(f[l][r],f[l][k]+f[k][r]+a[l]*a[k]*a[r];


上代码

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

using namespace std;

inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

const int maxn = 110;

int f[maxn][maxn];
int a[maxn];
int n;

int main()
{
  scanf("%d",&n);
  for (int i=1;i<=n;i++)
    scanf("%d",&a[i]);
  a[0]=1;a[n+1]=1;
  for (int i=2;i<n;i++) f[i-1][i+1]=a[i]*a[i-1]*a[i+1];
  for (int i=4;i<=n;i++)
  {
  	 for (int l=1;l<=n-i+1;l++)
  	 {
  	 	int r=l+i-1;
  	 	f[l][r]=1e9;
  	 	for (int k=l+1;k<=r-1;k++)
  	 	{
  	 	   f[l][r]=min(f[l][r],f[l][k]+f[k][r]+a[k]*a[l]*a[r]);
		 }
	   }
  }
  cout<<f[1][n]<<endl;
  return 0;
}



啦啦啦啦 目前就这么多


有部分区间dp的题目会在后续跟进~



如果我的博客能给各位爷带来任何一点的进步和收获,我都感到十分荣幸




共勉


from y_immortal



猜你喜欢

转载自blog.csdn.net/y752742355/article/details/80051222