剪裁序列
给定一个长度为 N 的序列 A,要求把该序列分成若干段,在满足“每段中所有数的和”不超过 M 的前提下,让“每段中所有数的最大值”之和最小。
试计算这个最小值
容易得到转移方程:
\[f(i)=\min_{0\le j<i,sum(i)-sum(j) \le m}\{f(j)+ \max_{j+1 \le k \le i} \{ a_k \} \}
\]
之后发现: \(f\)有单调性: \(f(i) \le f(i+1)\)
考虑单调性:
j要想成为可能的转移状态: 对于\(j-1\)和\(j\)就必须有
\[\max_{j+1 \le k \le i} \{ a_k \} \le \max_{j \le k \le i} \{ a_k \}
\]
也就是 \(a_j=\max_{j\le k\le i} \{a_k\}\)
也就是说 如果发现j不满足上述性质 就没有必要把j放入到决策集合
另一方面 如果j满足了上述性质 比j小的决策一定不合理
但是答案不具有单调性 所以要另外开一个优先队列来转移
需要注意的是,当i增大的时候,已经加入set的点要重新更新 这样时间复杂度大
因此 我们利用性质\(\max_{j+1 \le k \le i} \{a_i\}=q_{i+1}\)
这样就能够随着单调队列的更新过程动态更新set里的值
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <set>
#include <cmath>
#define ll long long
using namespace std;
const int N=1e5+10;
template <class T>
T read()
{
T x=0,f=0,c=getchar();
while(c<'0'||c>'9'){if(c=='-')f=1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return f?-x:x;
}
int a[N],st[N][20];
ll sum[N],f[N];
multiset< ll > s;
deque<int> q;
int n; ll m;
//st(i,j): the max value in [i,i+2^j-1]
void build()
{
int t=log2(n);
for(int i=1;i<=n;i++) st[i][0]=a[i];
for(int j=1;j<=t;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
}
int query(int l,int r)
{
int k=log2(r-l+1);
return max(st[l][k],st[r-(1<<k)+1][k]);
}
int main()
{
n=read<int>(); m=read<ll>();
for(int i=1;i<=n;i++) a[i]=read<int>();
for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];
build();
int L=1; ll S=0;
for(int i=1;i<=n;i++)
{
S+=1ll*a[i];
while(sum[i]-sum[L-1]>m) L++;
while(q.size()&&q.front()<L)
{
int t=q.front(); q.pop_front();
if(q.size()) s.erase( f[t]+a[q.front()] ) ;
}
while(q.size()&&a[q.back()]<=a[i])
{
int t=q.back(); q.pop_back();
if(q.size()) s.erase( f[q.back()]+a[t] );
}
if(q.size()) s.insert(f[q.back()]+a[i]);
q.push_back(i);
f[i]=f[L-1]+query(L,i);
if(s.size()) f[i]=min(f[i],*s.begin());
}
printf("%d\n",f[n]);
return 0;
}
技巧:
转移方程中两个式子有不同的单调性
维护决策集合 用数据结构处理