HDU6212 Zuma
题意简述:给定一个序列代表一行的黑白球,保证开始时没有任何超过两个的同颜色球相邻,你手上有足够多的黑白球,每次可以将其插入到任意位置,当三个及以上的同颜色球排在一起时会被消除,求清空整个序列需要的最少操作数。
区间 DP,区间 DP。
考虑到这是个序列问题,很容易考虑到区间 DP,区间 DP 的核心是大区间的答案通过小区间的答案合并获得,我们考虑 [ L , R ] [L,R] [L,R] 这个区间会怎么被消除。
最暴力也是最行之有效的是直接枚举一个断点 ( L ≤ k ≤ R ) (L \le k \le R) (L≤k≤R),分别消除 [ L , k ] [L,k] [L,k] 和 ( k , R ] (k,R] (k,R], 这也是最简单的区间 DP。
但这显然不是所有情况,考虑还有什么情况。
这题的核心问题在于每消除一段颜色,其他的球就会向这里“靠拢”,如果这个“靠拢”没有跨越子区间,那么显然可以有一个更长的区间包含了这次“靠拢”的最优解,这样就还是能通过上面的方程解决问题。
但是有且仅有一些操作"跨越"了子区间时,它不能通过枚举断点得到答案,如图:
在说这幅图之前,先得有一个认识:区间 DP 的正确性是因为也许一个区间的最优解是很多个小区间拼成的,但为什么我们可以两个区间合并得到呢?是因为这些小区间随着区间长度增大逐渐合并了。
上图的情况,简单来说就是它虽然还是两个区间分别求解,而是求解的两个区间并没有完整的覆盖整个答案,而是留下一些同色点“靠拢”,自动消除。基于上述的认识和论证,很容易发现这也是唯一的情况,所以分类讨论一下就好了。
实现的时候,为了方便可以采取类似“缩点”的操作,把同色节点缩成一个,这样一来整个区间就一定是黑点白点交替出现了。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define inf 20000000
using namespace std;
const int maxn=305;
int t,n,a[maxn],b[maxn],c[maxn],tot=0;
char s[maxn];
int dp[2000][2000];
int main(){
scanf("%d",&t);
int data_cnt=0,cnt=0;
while(t--){
cnt=tot=0;
memset(b,0,sizeof(b));
memset(s,0,sizeof(s));
for(int i=1;i<=200;i++){
for(int j=i;j<=200;j++){
dp[i][j]=(j-i+1)*2;
}
}
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=1;i<=n;i++){
a[i]=s[i]-'0';
}
for(int i=1;i<=n;){
b[++tot]=a[i];
c[tot]=i;
while(i<=n){
if(a[i]!=b[tot])
break;
i++;
}
c[tot]=i-c[tot];
}
for(int i=1;i<=tot;i++){
if(c[i]==2)
dp[i][i]=1;
else
dp[i][i]=2;
}
for(int i=1;i<=tot-1;i++){
for(int j=1;j<=tot-i;j++){
for(int k=j+1;k<=j+i;k++){
dp[j][i+j]=min(dp[j][i+j],dp[j][k-1]+dp[k][i+j]);
}
if((i)&1)
continue;
dp[j][i+j]=min(dp[j][i+j],dp[j+1][i+j-1]+(c[j]+c[j+i]==2));
if(c[j]+c[j+i]<=3){
for(int k=j+2;k<i+j;k+=2){
if(c[k]!=1)
continue;
dp[j][i+j]=min(dp[j][i+j],dp[j+1][k-1]+dp[k+1][i+j-1]);
}
}
}
}
printf("Case #%d: %d\n",++data_cnt,dp[1][tot]);
}
return 0;
}
Necklace
简述题意:给定一个有 n n n 个元素的环,其都有一个值 a i a_i ai,删去一些元素使得剩下的元素存在一种顺序可以以一个 a i = 10000 a_i=10000 ai=10000 分界,左边的数的单调不增,右边的数单调不减。
简单 DP+线段树,显然对于一个环套路的破环成链,对于每个 a i = 10000 a_i=10000 ai=10000 ,对于左边右边分别跑稍微改一下的权值最大上升/下降子串,答案取最大值。
然后这样的复杂度显然不能过题,我们把这东西转移方程写出来:
d p i = ∑ max d p j + a i ( a j ≤ a i ) dp_i=\sum \max dp_j+a_i (a_j \le a_i) dpi=∑maxdpj+ai(aj≤ai)
那么 a i a_i ai 是定值,区间 max 是老传统艺能了,直接线段树/树状数组等数据结构就好了。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#define inf 200000000
using namespace std;
const int maxn=5e4;
const int maxr=1e4;
int n,ls[maxn],rs[maxn],v[maxn],a[maxn*10],f1[maxn*10],f2[maxn*10];
inline void push_up(int i){
v[i]=max(v[ls[i]],v[rs[i]]);
}
inline void build(int i,int l,int r){
if(l==r) return ;
int mid=(l+r)/2;
ls[i]=i*2;
rs[i]=i*2+1;
build(ls[i],l,mid);
build(rs[i],mid+1,r);
push_up(i);
}
inline int query(int i,int l,int r,int L,int R){
if(L<=l&&r<=R){
return v[i];
}
int ans=0;
int mid=(l+r)/2;
if(L<=mid){
ans=max(ans,query(ls[i],l,mid,L,R));
}
if(R>mid){
ans=max(ans,query(rs[i],mid+1,r,L,R));
}
return ans;
}
inline void add(int i,int l,int r,int k,int t){
if(l==r){
v[i]=t;
return ;
}
int mid=(l+r)/2;
if(k<=mid){
add(ls[i],l,mid,k,t);
}
else{
add(rs[i],mid+1,r,k,t);
}
push_up(i);
}
int main(){
while(scanf("%d",&n)==1){
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[n+i]=a[i];
}
int ans=0;
for(int i=1;i<=n;i++){
if(a[i]==maxr){
memset(v,0,sizeof(v));
memset(f1,0,sizeof(f1));
memset(f2,0,sizeof(f2));
build(1,0,maxr);
for(int j=i+1;j<i+n;j++){
if(a[j]==maxr){
f1[j]=max(f1[j],f1[j-1]);
continue;
}
f1[j]=a[j]+query(1,0,maxr,a[j],maxr);
add(1,0,maxr,a[j],f1[j]);
f1[j]=max(f1[j],f1[j-1]);
}
memset(v,0,sizeof(v));
build(1,0,maxr);
for(int j=i+n-1;j>i;j--){
if(a[j]==maxr){
f2[j]=max(f2[j],f2[j+1]);
continue;
}
f2[j]=a[j]+query(1,0,maxr,a[j],maxr);
add(1,0,maxr,a[j],f2[j]);
f2[j]=max(f2[j],f2[j+1]);
}
for(int j=i;j<i+n;j++){
ans=max(ans,f1[j]+f2[j+1]+maxr);
}
}
}
printf("%d\n",ans);
}
}