Dinner
2019.8.22
2021.5.29
Description
一个 \(n\) 个点的环,有点权。将环拆成至多 \(m\) 段,使得权值和最大的段最小。
Solution
之前写的博客是假做法,还是太年轻了,打算重写一遍。做过的题都想不起来了,幸好最后还是想到了正解。
考虑二分答案,关键在于 check。肯定还是要断环成链,但是发现断环之后相邻段不能快速转移,即知道了 \([l,r]\) 的答案,不能快速算出 \([l+1,r+1]\) 的答案。然后之后想了很多假做法……最后还是想到了,二分了最大段长度之后,一个点能扩展的最远位置是确定的,而且可以通过 two-pointer 快速求出来,具体来说就是记一下前缀和,因为时间均为正,所以前缀和是单调的,然后就可以快速得到后继。若从 \(u\) 开始转移,我们只需要判断转移 \(m\) 此后是不是节点编号大于等于 \(u+n\)。
for(int tl=2;tl<=2*n;tl++){
while(hd<=tl-n) to[hd]=hd+n;
while(hd<tl&&S[tl]-S[hd-1]>x) to[hd]=tl;
}
这样转移就会快一些,复杂度 \(O(nm\log \sum|T|)\) 。然后我们发现转移只会向后指,实际上形成了一个树形关系。转移 \(m\) 次实际上是求 \(m\) 级父亲,所以想到可以树上倍增。于是复杂度 \(O(n\log^2 n)\)。
#include<stdio.h>
#include<algorithm>
using namespace std;
inline int read(){
int x=0,flag=1; char c=getchar();
while(c<'0'||c>'9'){if(c=='-')flag=0;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+c-48;c=getchar();}
return flag? x:-x;
}
const int N=1e5+7;
struct E{
int next,to;
}e[N];
int head[N],cnt=0;
inline void add(int id,int to){
e[++cnt]=(E){head[id],to};
head[id]=cnt;
}
int n,m,a[N],top=0,lim,mi,S[N],fa[N][12];
void dfs(int u){
for(int i=1;i<12;i++)
fa[u][i]=fa[fa[u][i-1]][i-1];
for(int i=head[u];i;i=e[i].next)
fa[e[i].to][0]=u,dfs(e[i].to);
}
for(int tl=2;tl<=2*n;tl++){
while(hd<=tl-n) to[hd]=hd+n;
while(hd<tl&&S[tl]-S[hd-1]>x) to[hd]=tl;
}
bool check(int x){
int hd=1; cnt=0;
for(int i=1;i<=2*n;i++) head[i]=0;
for(int tl=2;tl<=2*n;tl++){
while(hd<=tl-n){add(hd+n,hd);hd++;}
while(hd<tl&&S[tl]-S[hd-1]>x){add(tl,hd);hd++;}
}
while(hd<2*n){add(2*n,hd);hd++;}
fa[2*n][0]=2*n,dfs(2*n);
for(int s=1;s<=n;s++){
int u=s;
for(int i=11;~i;i--)
if(m&(1<<i)) u=fa[u][i];
if(u>=s+n) return 1;
}
return 0;
}
int calc(){
int lf=mi,rf=lim,ans=lim;
while(lf<=rf){
int mid=(lf+rf)>>1;
if(check(mid)) rf=mid-1,ans=mid;
else lf=mid+1;
}
return ans;
}
int main(){
freopen("dinner.in","r",stdin);
freopen("dinner.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n;i++)
lim+=(a[i]=a[i+n]=read()),mi=max(a[i],mi);
for(int i=1;i<=2*n;i++) S[i]=S[i-1]+a[i];
printf("%d",calc());
}