bzoj2448 挖油
题意
已知一个长度为n的数列,其中前x个数是1,后n-x个数是0.(x是未知的).
可以付出ti的代价询问第i个数是1还是0.
现在需要求x的值,采用最优策略,最坏情况下的代价是多少.
分析
定义f[i][j]表示已知i-1<=x<=j时,采取最优策略,直到查询出x的值的最坏代价.枚举下一次询问的位置k(i<=k<=j),那么\(f[i][j]=min(t[k]+max(f[i][k-1],f[k+1][j]))\)
显然f[i][k]随着k的增加是单调递增的,f[k][j]随着k的增加是递减的.所以对于[i,j]存在某个分界点p,使得对于所有\(i\le k\le p\),有\(f[i][k]\le f[k][j]\),对所有\(p<k<=j\),有\(f[i][k]\gt f[k][j]\).
另一个结论是,对于所有长度为len的区间[i,i+len],随着i的递增,p的位置也是递增的.(可以认为现在有两条直线,一条斜率为正,一条斜率为负,现在把其中一条向上抬,另一条向下按,想象交点横坐标的移动方向).
现在按照所有区间长度进行DP,先DP较短区间.
这里我之前猜了不少结论,比如可以三分决策点,比如四边形不等式,比如决策单调性,没一个是对的,感受自己的愚蠢
在DP时我们可以维护p.考虑k在p的左侧和右侧两种情况,就可以把方程里的max(f[i][k-1],f[k+1][j])干掉.
现在我们分别需要查询t[k]+f[i][k-1]和t[k]+f[k+1][j]的最小值.这个东西可以写线段树,但据说会卡常数.既然我不会写zkw线段树,那就写树状数组好了23333.开2n个树状数组,分别对应n个左端点和右端点,例如L[i]这个树状数组里存储所有左端点为i的状态(f[i][i],f[i][i+1],f[i][i+2]...)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=2005;
int g[maxn][maxn],f[maxn][maxn];
int n;
int t[maxn];
struct BIT1{
int c[maxn];
BIT1(){
memset(c,0x7f,sizeof(c));
}
void add(int x,int w){
for(;x;x-=x&(-x))c[x]=min(c[x],w);
}
int qmin(int x){
int ans=0x7f7f7f7f;
for(;x<maxn;x+=x&(-x))ans=min(ans,c[x]);
return ans;
}
}C1[maxn];
struct BIT2{
int c[maxn];
BIT2(){
memset(c,0x7f,sizeof(c));
}
void add(int x,int w){
for(;x<maxn;x+=x&(-x))c[x]=min(c[x],w);
}
int qmin(int x){
int ans=0x7f7f7f7f;
for(;x;x-=x&(-x))ans=min(ans,c[x]);
return ans;
}
}C2[maxn];
int main(){
int n;scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%d",t+i);
for(int i=1;i<=n;++i){
f[i][i]=t[i];
if(i+1<=n)C1[i].add(i+1,f[i][i]+t[i+1]);
if(i-1>=1)C2[i].add(i-1,f[i][i]+t[i-1]);
}
for(int len=1;len<n;++len){
int pt=1;
for(int i=1;i+len<=n;++i){
int j=i+len;
// f[i][j]=0x7f7f7f7f;
// for(int k=i;k<=j;++k){
// f[i][j]=min(f[i][j],max(f[i][k-1],f[k+1][j]);
// }
while(f[i][pt-1]<f[pt+1][j])++pt;
f[i][j]=min(C1[i].qmin(pt),C2[j].qmin(pt-1));
if(j+1<=n)C1[i].add(j+1,f[i][j]+t[j+1]);
if(i-1>=1)C2[j].add(i-1,f[i][j]+t[i-1]);
}
}
printf("%d\n",f[1][n]);
return 0;
}