为什么这两题要放到一起说呢,主要是这两题十分类似,用单调队列优化的方法是一样的,所以放在一起总结会比较印象深刻。
先说POJ3017----------Cut the Sequence
题目地址:http://poj.org/problem?id=3017
题目意思:
给你N个数和一个值M
可以将这N个数分成任意段,使得每段的和不大于M
要你使得每段的最大值加起来的和最小,求这个最小值
解题思路:
首先咱们提出一个状态转移方程:
f[i] = f[b[i]-1] + max(a[k]) b[i]<=k<=i
其中b[i]表示的是i属于的这块的头部,f[i]表示的以i结尾的块到目前为止的最大值的和
那么我们要求的和就是f[N]
但是这个复杂度太大了,我们得想想优化方法
注意到要求某一个窗口的最大值
那么咱们就用一个优先队列来维护一个最大值队列
则,就可以得到一个可行解f[i] = f[p-1]+a[q[l]],p就是分到i的这块的块头,而a[q[l]]就是这个优先队列的队首
但是这个不一定是最优解,可能咱们这个分块不一定是最科学的,那么我们就要从前面的结果中去找最优解来和这个可行解进行比较
如果遍历的话复杂度是O(n),但是如果用BST,可以把复杂度降一降,即O(logn)
那么最优解会是什么形式呢?
对于优先对列的q[j]来说,应该是f[q[j]]+a[q[j+1]],即a[q[j]]一定比后面的大,如果不是比后面的大,那我们可以把a[q[j]]划分到后面的这个块中,反正不影响,也许还可以找到更优的解(此中有真意,要仔细的思考思考)
那么确定了之后,我们怎么保证每段的和小于等于M,可以用下面的代码:
while(sum>m)
sum-=a[p++];
if(p>i) break;
这个p我们已经在之前的提到了,p就是i的这块的块头
如果还有不清楚的就看看代码:(这题不知为啥,用多case的数据处理方式就报WA,看来POJ的BUG还是很多啊)
#include<cstdio>
#include<cstring>
#include<set>
using namespace std;
#define LL long long
const int maxn = 111111+10;
int a[maxn],q[maxn];
LL sum,m,f[maxn],tmp;
int n;
multiset<int> bst;
int main()
{
//while(~scanf("%d%I64d",&n,&m))
scanf("%d%I64d",&n,&m);
{
int l,r,p;
l=0;
r=-1;
f[n]=-1;
p=1;
f[0]=0;
sum=0;
bst.clear();
//这个是为了满足每块的和要小于m
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
sum+=a[i];
while(sum>m)
sum-=a[p++];
if(p>i) break;
//要保证队列的单调性
while(l<=r && a[i]>=a[q[r]])
{
if(l<r)//删除队尾,就要删除由它相应产生的最优解
bst.erase(f[q[r-1]]+a[q[r]]);
r--;
}
q[++r] = i;
//添加一个放到集合里面去,有可能是最优解
if(l<r) bst.insert(f[q[r-1]]+a[q[r]]);
//根据前面求出的p,必须相应的后移队首,因为队首已经不能在优先队列中了
while(q[l]<p)
{
if(l<r) bst.erase(f[q[l]]+a[q[l+1]]);
l++;
}
f[i] = f[p-1]+a[q[l]];//由队首产生的一组可行解,不一定是最优解
tmp = *bst.begin();
if(l<r && f[i]>tmp)
f[i] = tmp;
}
printf("%I64d\n",f[n]);
}
return 0;
}
说完了上面的,再说下面的就容易多了
即POJ3245-------Sequence Partitioning
题目地址:http://poj.org/problem?id=3245
题目意思:
还是给你N个数对,一个限定值LIMIT
首先是把这N个数分成任意段,但是有要求
对于第p,q块,p<q
则Bp>Aq
此外,所有块的A最大值之和要小于LIMIT(是不是似曾相识?)
然后要你求出对于每块来说,B有一个和,要使所有块的和的最大值尽量小,求出这个值
首先来分块,有一些是必须要分的
对于i<j来说,如果Bi<Aj的话,那么从i~j必须和到一块去,这个是必须的,那么怎么做到这步呢
咱们可以先把B按从小到大排序,然后把A从N往前枚举,把B从排好的序往前枚举
只要B小于A,那么这个B是肯定要和A放到一起的,标记对打编号就OK,这也是A要倒着枚举的原因
具体的看代码
第一阶段的分块完成之后,我们就可以枚举这个要求的值,也就是POJ3017里面的M值
然后判断可行性就OK了,判断的代码和POJ3017很类似
所以这题相当于是两次分块
第一次是根据第一条件分了必须块,第二个就是根据枚举的M来调整,并判断可行性
下面上代码:
#include<cstdio>
#include<cstring>
#include<set>
#include<algorithm>
using namespace std;
const int maxn = 50000+100;
int a[maxn],b[maxn],q[maxn],f[maxn],p[maxn];
int n,limit,sum,tmp;
multiset<int> bst;
bool cmp(int v,int u)
{
return b[v]<b[u];
}
bool check(int m)
{
int l,r;
l=f[0]=0;
f[n]=r=-1;
int pos=1;
sum=0;
bst.clear();
for(int i=1;i<=n;i++)
{
sum+=b[i];
while(sum>m)
sum-=b[pos++];
if(pos>i)
return false;
while(l<=r && a[i]>=a[q[r]])
{
if(l<r)
bst.erase(f[q[r-1]]+a[q[r]]);
r--;
}
q[++r] = i;
if(l<r) bst.insert(f[q[r-1]]+a[q[r]]);
while(q[l]<pos)
{
if(l<r)
bst.erase(f[q[l]]+a[q[l+1]]);
l++;
}
f[i] = f[pos-1]+a[q[l]];
tmp = *bst.begin();
if(l<r && f[i]>tmp)
f[i]=tmp;
}
return f[n]<=limit;
}
int main()
{
scanf("%d%d",&n,&limit);
int i,j;
for(i=1;i<=n;i++)
{
scanf("%d%d",&a[i],&b[i]);
q[i]=p[i]=i;
}
sort(p+1,p+1+n,cmp);
for(j=1,i=n;i>0;i--)
{
while(j<=n && b[p[j]]<=a[i])
q[p[j++]]=i;
}
int l,r;
for(i=1,j=1;i<=n;i=l,j++)
{
a[j]=a[i],b[j]=b[i];
for(l=i+1,r=max(q[i],i);l<=r;l++)
{
a[j] = max(a[j],a[l]);
b[j] +=b[l];
r=max(r,q[l]);
}
}
l=0,r=0x3f3f3f3f;
int ans;
n=j-1;
while(l<=r)
{
int mid = (l+r)>>1;
if(check(mid))
ans=mid,r=mid-1;
else
l=mid+1;
}
printf("%d\n",ans);
return 0;
}
另外要感谢: http://blog.youkuaiyun.com/fp_hzq,我这两题都是根据他的博客学的,
详见http://blog.youkuaiyun.com/fp_hzq/article/details/7980663,http://blog.youkuaiyun.com/fp_hzq/article/details/7982821