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
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;kcnt+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<
QWQ早期码风比较清奇 嗯 (竟然不定义l和r 稍微意淫一下)
不过,这道题在hdu上(hdu3506)n^3的时间复杂度过不了
那么我们就需要考虑,四边形不等式优化!!
这里引用一个dalao的博客! https://blog.csdn.net/noiau/article/details/72514812
可以理解为一句话,交叉小于包含,即交叉的两个区间,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
#include
#include
#include
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
#include
#include
#include
#include
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
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
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
poj2955
题目大意:
给定一个只含小括号和中括号的序列,求一个最长的合法序列。
其中() [] ()[] (()) 等 是合法的
)( (] 等 是不合法的
由于两个本身合法的序列拼在一起也是合法的序列,所以我们自然而然的就想到
令f[l][r]表示l~r这个区间的最长合法序列的程度
根据a[l] a[r] f[l+1][r-1]进行初始化
然后枚举断点,进行转移
这里有一个小tip就是
可以把左右括号分别变成正负的数字,便于判断
二话不说
直接上代码
#include
#include
#include
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
#include
#include
啦啦啦啦 目前就这么多
有部分区间dp的题目会在后续跟进~
如果我的博客能给各位爷带来任何一点的进步和收获,我都感到十分荣幸
共勉
from y_immortal