[bzoj3141][HNOI2013]旅行

题目描述

在遥远的HX 国,住着一个旅行家小L,他希望骑着他的自行车游遍全国。在这个国家中,每个城市都有一个编号,共有n 个城市,编号从1 到n。有的城市没有小L 想去的景点,而有的城市有且仅有一个小L 想去的景点,所有城市都是这两种情况之一。小L 非常热爱

信息学,他编写程序给他的旅行安排了一条最短路线以到达所有他想去的景点(所以他旅行线路上城市编号是乱序的):他第1 个到达的城市编号为a1,第i 个到达的城市编号为ai,最后到达城市an 结束这次旅行。小L 希望用恰好m 个月(m < n)的时间完成这次旅行,所以他需要制定一个理性的旅行计划。

当他到达一个城市时,如果这个城市有他想要去的景点,他会因此获得1 点快乐值;但是若到达的城市没有他想去的景点,他会因旅途的疲惫得到1 点疲劳值。一个月的时间足够他游玩任意多个城市,但他也希望拿出一定时间来休息。他每个月总是在本月所达到的最后

一个城市休息(但如果这个城市有景点,那么小L 总会游玩完这个景点再休息)。当然,小L 希望每个月都能有一定的旅行任务,即便这个月他所到达的城市中并没有他想去的景点,换句话说,每个月他都会至少到达一个新的城市。

小L 无法自己安排旅行计划,所以求助于你。你需要告诉他一个序列:

x1, x2, …, xm

xi 表示小L 第i 个月休息时,他所在的城市编号。由于他最后一个月必须完成他的旅行,所以xm 肯定等于an。例如,设n = 5,m = 3,(a1, a2, a3, a4, a5) = (3, 2, 4, 1, 5),(x1, x2, x3) = (2,1, 5),这意味着:第1 个月先后到达3 号和2 号城市,并在2 号城市休息;第2 个月先后到达4 号和1 号城市,并在1 号城市休息;第3 个月到达5 号城市,并在5 号城市休息。

这样的方案序列有很多种,设每种方案序列中的第i 个月旅行中当月获得的快乐值与疲劳值的差的绝对值为di,设第k 种方案序列中求出的d1, d2, …, dm这个m个值的最大值为ck,小L 希望所选择的方案序列的ck 在所有方案序列中是最小的。

事实上,可能有多个方案序列的ck 达到并列最小值。由于小L 喜爱编程,他患上了一定的强迫症(虽然他自己认为他的强迫症让他炫的发黄),他希望给他的序列是这多个方案中字典序最小的。。

Tips:比较两个序列字典序即比较第一个不相同数字的大小,如1, 2, 3, 4 < 1, 2, 4, 3。

结论

令s[i]表示后缀和。
当s[1]!=0时
ans=ceil(abs(s[1])/m)
当s[1]=0时且s[i]=0的个数>=m ans=0否则ans=1。
证明:当s[1]=0时且s[i]=0的个数>=m ans=0显然可证。
比如s[1]=5时,我们一定能找到这样一些点使后缀和分别为1,2,3,4
那么显然ans=ceil(abs(s[1])/m)
当s[1]=0时且s[i]=0的个数 < <script id="MathJax-Element-1" type="math/tex"><</script>m时,找到所有0点,然后在他们之间取。一定能够造成ans=1。
如何求方案?
当s[1]=0时且s[i]=0的个数>=m时我们直接跑单调队列即可。
其余情况,我们考虑解决这样的子问题
find(x,S,m)表示在x~N中,寻找m个分割点,当前所有元素和为S。
分割点k需要满足3个条件:
1、abs(S-s[k+1])<=ans
2、ceil(abs(s[k+1]),m-1)<=ans
3、N-k>=m-1
每次都要找出符合条件a[k]最小的分割点k。
为了更好的满足1、2,我们对于每一个值维护a[i]单调递增的单调队列。也就是说,第i个单调队列里的任何一个元素k满足s[k+1]=i。
对于条件1,可以枚举第S-ans~S+ans这些单调队列,分别取出最优值作比较。
对于最后一个条件,可知第一次满足后就一定永久满足,现将所有已满足元素入队,每次m减一时都放入最后一个满足条件的元素即可。
当枚举到第i个单调队列时,不能满足第2个条件就跳过。
每次检验单调队列队首是否合法,即检验队首元素是否在x~N内。
由于防止爆空间,队列要自己手打,可采用类似链表的形式。
C++不支持负数下标,变为非负数即可。

参考程序

#include<cstdio>
#include<algorithm>
#include<set>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
using namespace std;
const int maxn=500000*2,maxm=200000+100;
set<int> fs;
int a[maxn],b[maxn],s[maxn],L[maxn*2],R[maxn*2],next[maxn],pre[maxn],c[maxm],id[maxn],dl[maxn];
int i,j,k,l,t,n,m,ans,last,top,cnt;
int ceil(int x,int y){
    if (x%y==0) return x/y;
    else return x/y+1;
}
void push(int x,int y){
    if (!L[x]){
        L[x]=R[x]=y;
        return;
    }
    next[R[x]]=y;
    pre[y]=R[x];
    R[x]=y;
}
void fly(int x,int y){
    if (L[x]==R[x]&&L[x]==y){
        L[x]=R[x]=0;
        pre[y]=next[y]=0;
        return;
    }
    if (L[x]==y){
        pre[next[y]]=0;
        L[x]=next[y];
        return;
    }
    int z2=next[y],z1=pre[y];
    pre[z2]=z1;
    next[z1]=z2;
}
void find(){
    int x=1,S=s[1],t,i,l,k;
    while (m>1){
    t=s[n-m+2]+n;
    push(t,n-m+1);
    while (L[t]!=R[t]&&a[R[t]]<a[pre[R[t]]]) fly(t,pre[R[t]]);
    l=1000000;
    fo(i,S-ans,S+ans){
        if ((t=i+n)<0) continue;
        if (L[t]==0) continue;
        if (ceil(abs(i),(m-1))>ans) continue;
        while (L[t]!=0&&L[t]<x) fly(t,L[t]);
        if (L[t]==0) continue;
        if (a[L[t]]<l){
            l=a[L[t]];
            k=L[t];
        }
    }
    c[++top]=a[k];
    x=k+1,S=s[k+1],m--;
    }
}
int main(){
    scanf("%d%d",&n,&m);
    fo(i,1,n){
        scanf("%d%d",&a[i],&b[i]);
        b[i]=b[i]?1:-1;
        id[a[i]]=i;
    }
    s[n]=b[n];
    fd(i,n-1,1){
        s[i]=b[i]+s[i+1];
        if (!s[i]) cnt++;
    }
    if (!s[1]&&cnt>=m){
        k=1,l=0;
        fo(i,2,n)
            if (s[i]==0){
                cnt--;
                while (k<=l&&a[i-1]<a[dl[l]]) l--;
                dl[++l]=i-1;
                if (cnt<m) c[++top]=a[dl[k++]];
            }
        c[m]=a[n];
        fo(i,1,m) printf("%d ",c[i]);
        return 0;
    }
    if (s[1]==0) ans=1;else ans=ceil(abs(s[1]),m);
    fo(i,1,n-m){
        t=s[i+1]+n;
        push(t,i);
        while (L[t]!=R[t]&&a[R[t]]<a[pre[R[t]]]) fly(t,pre[R[t]]);
    }
    find();
    c[++top]=a[n];
    fo(i,1,top) printf("%d ",c[i]);
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值