区间DP
P1063 能量项链
题目描述
- 给定一串首尾相连的能量珠串
- 按照该计算规则进行合并:如果前一颗能量珠的头标记为(m),尾标记为(r),后一颗能量珠的头标记为(r),尾标记为(n),则聚合后释放的能量为(m imes r imes n),新产生的珠子的头标记为(m),尾标记为(n)。
- 求最终合并为一个珠子的时候释放的能量的最大值
思路分析
- 首先因为只是一个串串,所以我们肯定不好弄,所以我们可以生成一个线性的区间来代替这个串串
- 那么根据这个规则,我们分为好几个小区间进行(dp),类似于弗洛伊德的算法,枚举不同长度的区间进行比较大小
- 我们很显然的可以知道从(i~n+i-1)是一个完整的能量珠串(减去(1)是因为他自己已经有了),所以我们可以根据这个做dp了
- 枚举长度(k),所以我们可以看出在(i~i+k)这个区间内,我们可以找一个点(j)来分割开,相当于已经合并完的两个珠子(i~j)和(j+1~i+k),最终在进行合并
状态转移方程设计
- 设(f_{i,j}为合并第)i~j$个石子的最优值
- 所以我们根据思路可以推导出式子
[f[i,i+k]=max(f[i,i+k],f[i,j]+f[j+1,i+k]+left_i imes right_j imes right_{i+k}) (jin [i,i+k))
]
代码实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=410;
int f[N][N];
int n;
struct node{
int x,y;
}a[N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i].x;
a[i-1].y=a[i].x;
}
a[n].y=a[1].x;
for(int i=1;i<n;i++)
a[n+i]=a[i];//使其成为一条链,方便操作
//i~i+n就可以看成一个完整的链
memset(f,0,sizeof(f));
//f[i][j]表示合并第i~j个石子的最优值
for(int k=1;k<=n;k++)//枚举长度
for(int i=1;i<2*n-k;i++)//从第i个石子开始断开,
//那么i~i+n就是一条线
for(int j=i;j<i+k;j++)
//在i~i+k中任意选一点j作为分界点,然后就可以分成
//i~j和j+1~i+k这两段
//首先这两段里面的都合并起来,然后最后(i,j)(j+1,i+k)端点序号为这两个的
//珠子在尽心合并
{
f[i][i+k]=max(f[i][i+k],f[i][j]+f[j+1][i+k]+a[i].x*a[j].y*a[i+k].y);
}
int ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,f[i][i+n-1]);
cout<<ans<<endl;
return 0;
}
P1880 [NOI1995]石子合并
题目描述
在一个圆形操场的四周摆放 (N) 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的(2)堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 (N) 堆石子合并成 (1) 堆的最小得分和最大得分。
思路分析
- 和能量项链这个题目相类似,仍然是一个环形的区间dp,我们可以从其中的任意一堆石子开始操作
- 我们可以把这个多开(n)的数组空间,将他弄成线性进行解决
状态转移方程设计
-
区间dp,所以我们最常见的方法是状态转移方程设置为区间的形式
-
最常见的做法为在一个大的区间内找两个区间并进行合并,求最大值
-
这个题我们通过数据可以发现,当区间([i,j])中两个小的区间([i,k],[k+1,j])合并时,它这次合并的得分正好为第(i)堆到第(j)堆的石子的总个数
所以我们设(s_i)为前(i)堆石子的前缀和 -
我们可以设置(f[i][j]表示从第)i(堆到第)j$堆合并成为(1)堆时的区间最值,可以得到以下的状态转移方程式
[f_{max}[i][j]=max(f_{max}[i][j],f_{max}[i][k]+f_{max}[k+1][j]+s_j-s_{i-1}) (kin [i,j))
]
[f_{min}[i][j]=min(f_{min}[i][j],f_{min}[i][k]+f_{min}[k+1][j]+s_j-s_{i-1}) (kin [i,j))
]
代码实现
#include<iostream>
#include<queue>
#include<stack>
#include<cstdio>
#include<cstring>
#include<queue>
#include<map>
#include<algorithm>
using namespace std;
const int N=5e2+9;
int a[N];
int n;
int s[N];//前缀和
int fmax[N][N],fmin[N][N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[i+n]=a[i];
}
for(int i=1;i<=n*2;i++)
s[i]=s[i-1]+a[i];
//memset(fmin,0x3f3f3f,sizeof(fmin));
for(int l=1;l<n;l++)
for(int i=1,j=i+l;(i<n+n)&&(j<n+n);i++,j=i+l)
{
fmin[i][j]=0x3f3f3f3f;
for(int k=i;k<j;k++)
{
fmax[i][j]=max(fmax[i][j],fmax[i][k]+fmax[k+1][j]+(s[j]-s[i-1]));
//两边i~k和k+1~j,最后一次合并的时候加起来得到的的分数正好是
//i~j的和
fmin[i][j]=min(fmin[i][j],fmin[i][k]+fmin[k+1][j]+(s[j]-s[i-1]));
}
}
int amax=0,amin=0x3f3f3f3f;
for(int i=1;i<=n;i++)
{
amax=max(amax,fmax[i][i+n-1]);
amin=min(amin,fmin[i][i+n-1]);
}
cout<<amin<<endl;
cout<<amax<<endl;
return 0;
return 0;
}
P3146 [USACO16OPEN]248 G
题目描述
(一个因为翻译而WA的“毒瘤”题)
给定一个长度为(n)的区间,在区间内相邻的且数字大小相同的两个数字可以合并的到一个比它(+1)数字
询问可以合并成的最大数值为多少
思路分析
-
一个线性区间dp,我们依旧是在区间内做处理
-
在一个区间内,枚举长度,并在这个区间内找一个分割点,是这个点两边的数值是相等的,然后进行大小比较
状态转移方程设计
-
我们可以设(f[i][j])为区间([i,j])内的合并出来的最大值
-
由此可以得到状态转移方程(状态可以根据需要灵活变化,此方程取(j)为分界点)
[f[i][i+k]=max(f[i][i+k],f[i][j]+1) (f[i][j]=f[j+1][i+k])
]
代码实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#include<cmath>
using namespace std;
const int N=5e2+9;
int f[N][N];
int num[N];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>num[i];
f[i][i]=num[i];
}
int ans=0;
for(int k=1;k<=n;k++)//枚举区间长度
for(int i=1;i+k<=n;i++)//确保右端点在范围内
for(int j=i;j<i+k;j++)//保证分割的界限在范围内
{
if(f[i][j]==f[j+1][i+k])//判断两边是否相等
f[i][i+k]=max(f[i][i+k],f[i][j]+1);
//可以改成 f[i][i+k]=max(f[i][i+k],f[j+1][i+k]+1);
ans=max(f[i][i+k],ans);//在过程中找答案,节省时间
}
cout<<ans<<endl;
return 0;
}
P4170 [CQOI2007]涂色
题目描述
- 一开始给你一个空序列,可以使一个字母连续覆盖相邻的任意等长的区间,求最少有几次可以得到目标状态
思路分析
- 因为是字符串,输入的时候处理一下让他的编号从(1)开始,好进行处理
- 首先,每个长度为(1)的区间都赋值为(1),因为他需要进行一次涂色
- 其次,我们可以发现的是,当我们枚举一个区间时,如果左右端点是一样的,那么我们可以对左右端点分别做操作,比较一下左端点右移(1)的区间去覆盖左端点次数小还是右端点左移动(1)的区间去覆盖右端点所使用的次数少。
- 最后进行正常的区间断点枚举,知道找出最小的方案为止
状态转移方程设计
- 正常的状态转移方程,设(f[i][j])为区间([i,j])变成最终状态所需要的最小次数
- 那么可以得到状态转移方程:
[f[i][j]=min egin{cases} f[i+1][j],f[i][j-1] (s[i]=s[j])
\
\ f[i][j],f[i][k]+f[k+1][j] (s[i]!=s[j])
end{cases}]
代码实现
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#include<iostream>
using namespace std;
const int N=1e2+9;
int f[N][N];
char s[N];
int main()
{
cin>>(s+1);
memset(f,0x3f3f3f3f,sizeof(f));
for(int i=1;i<=strlen(s+1);i++)
f[i][i]=1;//开始可以被涂一次//bingo
for(int k=1;k<=strlen(s+1);k++)//枚举区间
{
for(int i=1;i+k<=strlen(s+1);i++)
{
if(s[i]==s[i+k])//两端点相等,所以我们就在首次涂色的时候多涂上一层,
//看看是涂到左边端点花费少还是涂到右边端点花费少
f[i][i+k]=min(f[i+1][i+k],f[i][i+k-1]);
else for(int j=i;j<i+k;j++)
f[i][i+k]=min(f[i][i+k],f[i][j]+f[j+1][i+k]);
//如果两个端点一样,那么就是两块区间相加求最小值
//因为当你左右两边不一样是,一定会左右均刷一次,
//然后对于中间的,就看一看是否有一样的就可以
//dp枚举可以考虑到上述情况
}
}
cout<<f[1][strlen(s+1)]<<endl;
return 0;
}
P4290 [HAOI2008]玩具取名
思路分析
- 因为给的每一个转化的字符串是两个值,所以我们只需要通过枚举分别都可以被一个字母表示的两个小区间,然后看一下这两个字母是否可以被一个字母来代替,也就是找一找是否可以用一个字符来代替整个区间
状态转移方程设计
- (f[i][j][k])表示区间([i,j])可以通过(k)转化过来
- (can[i][j][k])表示(i,j)可以通过(k)转化过来((z1+z2->z))
[f[i][i+k][z]=true (f[i][j][z1]=true,f[j+1][i+k][z2]=true,can[z][z1][z2]=true)
]
代码实现
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
using namespace std;
const int N=209;
bool f[N][N][5],can[5][5][5];//f表示区间[i,j]可以通过k转化过来
//can表示i,j可以通过k转化过来
int le[5];//存长度
char s[N],c[5];
int ques(char s)
{
if(s=='W') return 1;
if(s=='I') return 2;
if(s=='N') return 3;
if(s=='G') return 4;
}
int main()
{
for(int i=1;i<=4;i++) cin>>le[i];
for(int i=1;i<=4;i++)
{
for(int j=1;j<=le[i];j++)
{
cin>>c;
can[i][ques(c[0])][ques(c[1])]=true;//表示i可以从这俩转化过来
}
}
cin>>(s+1);
int len=strlen(s+1);
for(int i=1;i<=len;i++)
f[i][i][ques(s[i])]=true;
for(int k=1;k<=len;k++)
for(int i=1;i+k<=len;i++)
for(int j=i;j<i+k;j++)
for(int z=1;z<=4;z++)//枚举可以代替z1,z2的数
for(int z1=1;z1<=4;z1++)//枚举z1
for(int z2=1;z2<=4;z2++)//枚举z2
{
if(f[i][j][z1]&&f[j+1][i+k][z2]&&can[z][z1][z2])
//这个方程表示如果区间[i,j]可以被z1表示
//并且区间[j+1,i+k]可以被z2表示
//同时z可以与z1,z2转化
//那么[i,i+k]这个区间就可以被z来表示
f[i][i+k][z]=true;
}
bool flag=0;
if(f[1][len][1]) {flag=1,cout<<'W';};
if(f[1][len][2]) {flag=1,cout<<'I';};
if(f[1][len][3]) {flag=1,cout<<'N';};
if(f[1][len][4]) {flag=1,cout<<'G';};
if(!flag)
cout<<"The name is wrong!"<<endl;
return 0;
}
状压DP
P1896 [SCOI2005]互不侵犯
思路分析
- 首先看一下数据范围,这是一个状态压缩动态规划,所以我们就考虑用二进制来进行处理
- 我们可以发现,每一个位置的国王数量最多是(1),所以我们就可以用一个二进制串串来表示每一行的国王分布情况,
- 一开始我们可以预处理一下每一行符合标准的状态(situ_{i}),然后把它这一行的国王数量(sum_{i})和二进制串存到一个数组中,便于枚举
- 然后我们考虑一下这一个状态如何转移
- 按照题目中的条件所说的,每一个国王的上下左右及左上下,右上下都不可以放人!!!
- $situ_j $ & (situ_k) 如果不为零,说明上下有交叉的
- $situ_j $ & (situ_k<<1),如果不为零,说明右上放了人
- (situ_j<<1) & (situ_k),如果不为零,说明左上方放了人
- 这里还有一个小技巧就是,当我们枚举一行的时候,我们只需要考虑上一行是否符合这个状态就好了,下一行的状态可以转到下一行的时候在考虑这一行的状态来判断即可
状态转移方程设计、
设(f[i][j][k])表示第(i)行,状态为第(i)行,状态为(j)时,前(i)行的一共放了(k)个国王的方案数
得到以下解题思路
[f[i][j_1][k]=sum f[i-1][j_2][k-sum[i]]
]
代码实现
#include<iostream>
#include<cstdio>
#include<string>
#include<queue>
#include<stack>
#include<map>
#include<algorithm>
#define int long long
using namespace std;
const int N=11;
const int M=2009;
int n,num;
int cnt;//状态的指针
int situ[M];//可用的状态
int sum[M];//求每一个状态所包含的1的数量
int f[N][(1<<N)][N*N];//表示第i行,状态是j,放置了k个棋子时的状态...
void search(int he,int gs,int pif)//表示状态,表示1的个数,表示当前为第几位
{
if(pif>=n)
{
situ[++cnt]=he;
sum[cnt]=gs;
return;
}
search(he,gs,pif+1);//这个就是表示当前位数没有选,要选择与他相邻的位数
search (he+(1<<pif),gs+1,pif+2);//当前为要选的第pif位,所以就在第pif位上标上个1;
//表示在这个地方有一个国王
}
signed main()
{
cin>>n>>num;
search(0,0,0);
for(int i=1;i<=cnt;i++)
f[1][i][sum[i]]=1; //第二唯为状态的下标不是状态
for(int i=2;i<=n;i++)
for(int j=1;j<=cnt;j++)//枚举当前的状态
for(int k=1;k<=cnt;k++)//枚举上一个的状态
{
if(situ[j]&situ[k])continue;
if(situ[j]&(situ[k]<<1)) continue;
if((situ[j]<<1)&situ[k]) continue;
for(int l=num;l>=sum[j];l--)
f[i][j][l]+=f[i-1][k][l-sum[j]];
}
int ans=0;
for(int i=1;i<=cnt;i++)
ans+=f[n][i][num];
cout<<ans<<endl;
return 0;
}