【HDU3415】Max Sum of Max-K-sub-sequence,思路+解题报告+AC代码+自虐般疯狂吐槽【0.3%达成!】

5 篇文章 0 订阅
1 篇文章 0 订阅
本文详细介绍了利用单调队列解决HDU3415问题的过程,包括思路、解题报告、AC代码及常见误区解析。通过维护一个单调递增的队列,存储前缀和的下标,确保队列内的元素个数不超过K,从而求解最大子序列和。
摘要由CSDN通过智能技术生成
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <iostream>
using namespace std;
const int MAX_SIZE = 100010 * 2;
const int INFINITE = 99999999;
int num[MAX_SIZE];
int sum[MAX_SIZE];
int q[MAX_SIZE];
int ind[MAX_SIZE];
int begin;
int end;
/**
        Problem: HDU3415-Max sum of max-k sequence 【0.3%达成!】
        Thanks to : LPP学长,YL学姐,辛苦了两位= =开化我这种愚人
        Reference:http://www.cppblog.com/wuxu/archive/2010/08/24/124575.aspx
        Knowledge Point:单调队列
        关于单调队列还有一篇好的Reference看,入门级的但是讲的足够详细了。
        http://www.docin.com/p-49960245.html 用单调性优化优先队列。
        Fucking Misunderstanding:
            傻逼错误1:误把sum(i,j)写作了sum(i,j) = sum(i) - sum(j)!实际上这样就把元素j抛出去了,应该是sum(i)-sum(j-1)!
        Thought:
        【大半夜写的,发现bug欢迎fix。。。拿出证据,而不是“我觉得”三个字儿,谢谢】

        好吧,都说过了是单调队列,递增的。
        这里就当做对单调队列的一个大summary好了。
        所谓单调队列,就是其中的内容是单调的,其中元素对应的数组下标也是单调的。
        这里的单调指的不是单调上升就是单调下降。
        单调队列的名字中虽然带了队列两个字,但是跟队列还是有区别的,不过也有共同之处。
        共同之处就是在头(front)的位置取元素,由于单调性可以保证一类问题在单调队列的头处总有最优解,所以是可以这么做的。
        不同之处就是单调队列的头部是可以删除元素的(我习惯用front++来实现),当然,跟队列一样,也可以从尾部添加、删除元素(但是头部不能添加元素哦!)
        那么,这道题的解题思路在哪儿呢?
        就是将给出数组的A[i]求和,Sum[i]表示从[0,i]区间数组A[]元素的和。
        那么,如果求[i,j]区间的和,只要用sum[i] - sum[j-1]就好(因为如果减去的是sum[j]那么会把元素j也减掉。。妈的,误区!)

        我们用一个单调队列将sum的序号存储在单调队列数组q[]中,并且保证rear - front <= K,这样的话我们保存的
        就是连续个sum数组[]的下标,其中的值[ sum[front] , sum[rear-1] ]为sum[m] (  m属于区间[i,j], j - i <= K )
        至于rear为什么要-1,则是因为rear始终指向空的空间用来存储sum的新值,记得么?队列的基本性质。
        这样就好办了
        我们可以将N个数存储在A[1]->A[N]中,A[0]为0,这样是为了方便计算sum[]
        sum的计算是这样的:
        for(int i = 1 ; i<= N ; i++)
            sum[i] = sum[i-1] + A[i];
        记得要把N个数的开头K个数(其实是K-1个……因为 N到N+K-1(包括两端)总共是K个数……虽然相减等于K-1,但是由于包括端点还得+1呢~)
        放在A[N+1]到A[N+K]中(哎呀,偷懒,就这么放了,不影响结果!)
        sum也要算到sum[N+K-1];

        关键代码就在 ↓这里,恶心的地方也在这里
        为什么是 sum[i-1] < sum[ q[rear-1] ]呢?sum[ q[rear-1] ]好解释
        就是队内队尾元素的sum值嘛,但是为什么要sum[i-1]呢?
        答案就是在底下的res = ( sum[i] - sum[ q[front] ] )里面。
        而q[rear++]=i-1也提示了我们这一点。
        只有在遍历到sum[i]的时候,q[rear]存储的是sum[i-1]的下标,才能根据当前的
        sum[i]判断是否取到了最大值。证明见后
        for(int i = 1 ; i <= N + K - 1 ; i++ )
        {
            while(front < rear && sum[i-1] < sum[ q[rear-1] ])
                rear--;
            q[rear++]=i-1;
            if( sum[i] - sum[ q[front] ] > res )
            {
                begin = q[front] + 1;
                end = i;
                res = sum[i] - sum[q[front]];
            }
        }

        证明刚才说的那个q[rear++] = i-1;

        初始:
            如果q[rear++]一直得到执行而从未发生rear--这种事(初始么!)
            那么q[rear-1]存储的实际上是sum[i-2]的下标,判断sum[i-2] > sum[i-1]的话
            rear--后退,让队尾元素变为sum[i-1]下标,亦即q[rear++] = i - 1,保持了q[]中sum值和下标的单调性。
        维持:
            rear--得到执行一次之后,rear指向的是sum[i-1]未更新前的最大元素,那么如果我们将sum[i-1]和sum[q[rear]]比较,尾部不断向前移动,就是存储下标对应的元素逐渐减小
            也就是rear--,一直找到sum[i-1]比sum[q[rear-1]]要大的时候,我们才q[rear++] = i - 1;
        终止:
            根据一、二可以知道,这样是始终维护了队列的单调性的。


        【耗时两个小时的证明,为毛就把i-1放在q[]而不是i呢?该部分为我一根筋的吐槽,非虐勿看!】
        证明如下:
        妈的,主要是这里想不明白真他妈讨厌!
        想一下我们用单调队列来干毛啊,保证sum[q[front]]是这个区间内从A[1]到该区间开始端点和的最小值啊
        这样我们sum[i] - sum[q[front]]才是最优啊。
        如果你保存的是i的话,那么就得改成
        sum[i]-sum[q[rear-1]]+num[q[rear-1]]是最优才可以。
        那怎么改?
        那就判断的话。。。
        第一、不能跟sum[i]判断,要用(sum[q[rear-1]]-num[q[rear-1]])和sum[i]-num[i]判断
        if( front < area && ( sum[i]-num[i] ) < (sum[q[rear-1]] - num[q[rear-1]])
                本质上不还是跟放i-1是一样的么!!
        sum[i]是用来确定最优解的,如果
        sum[i] < sum[q[rear-1]] 判断的话,那么,那得到的是毛啊,你把用来确定最优解的东西用来判断?!
        你得在优先队列里得到 i 点之前的 sum[k]( num[1] -> num[k]的和的最小值 ) 从而保证q[front]是最优的啊
        这也就是为什么保证q[]里面是单调递增的原因啊,怎么能直接把 i 直接扔进去啊,到了 i 点应该是让 i 点之前的和最小啊
        i点是用来转移的!看你自己的WA到死的代码,判断最优解的时候不也是
        sum[i] - sum[q[front]] + num[q[front]]么,你把sum[i]用来转移的话,你就不能直接扔i进去啊
        那不破坏了整个队列在[j,i-1] (j为起点,j = i - K)的最优状态么。。换言之那TM根本就不是最优状态

        为神马破坏最优状态了呢?因为有 while(front < rear && i - q[front] > K ) front++ 这句话撑着啊,你要把sum[i]放进来是不是得丢出去一个
        元素啊?而且还是front啊!你把最优的丢出去了啊!
        其实也不是最优的,因为从初始化的时候就是错的=。=
        总之,如果q[]放i并且还用sum[i]进行转移的话,那每次得到的都TM不是最优的状态,因为第一个元素(front)被扔出去了!

                    娘希匹的!Q.E.D!
        【卧槽,我是不是精分了?】

        第二、你的起始点确定累不死你。。。

**/
int solve(int N,int K)
{
    int res = -INFINITE;
    int front = 0;
    int rear = 0;
    for(int i = 1 ; i <= N+K-1  ; i++)
    {
        while(front < rear && i - q[front] > K )  //不用写等号!因为k 不等于0
            front++; //保持距离在k
        /**
            主要就是下面那行代码让我费劲了,果然智商是硬伤
            首先,简单的,q[rear-1],取得是队尾元素(rear始终指的是队列中的空地方)
            而sum[i-1]的原因则是因为我们要用sum[i]来判断当前队列和是否是最优的!证明在解题报告中给出!
        **/
        while(front < rear && sum[i-1] < sum[ q[rear-1] ])
            rear--;
        q[rear++]=i-1;
        if( sum[i] - sum[ q[front] ] > res )
        {
            begin = q[front] + 1;
            end = i;
            res = sum[i] - sum[q[front]];
        }
    }
    if( end > N )
        end = end - N;
    return res;
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("B:\\acm\\SummerVacation\\DP-II\\F.in","r",stdin);
    freopen("B:\\acm\\SummerVacation\\DP-II\\F.out","w",stdout);
#endif
    int T,N,K;
    while(scanf("%d",&T) != EOF)
    {
        for(int t = 1 ; t <= T; t++)
        {
            scanf("%d%d",&N,&K);
            num[0] = 0;
            sum[0] = 0;
            memset(num,0,sizeof(num));
            memset(sum,0,sizeof(sum));
            memset(q,0,sizeof(q));
            memset(ind,0,sizeof(ind));
            begin = 0; end = 0;
            for(int i = 1 ; i <= N ;i++)
            {
                scanf("%d",&num[i]);
                sum[i] = num[i] + sum[i-1];
            }
            for(int i = N+1; i <= N+K ; i++)
            {
                num[i] = num[i-N];
                sum[i] = num[i]+sum[i-1];
            }
            int res = solve(N,K);
            printf("%d %d %d\n",res,begin,end);
        }
    }
#ifndef ONLINE_JUDGE
    fclose(stdin);
    fclose(stdout);
#endif
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值