[POI2010]KLO-Blocks——一道值得思考的题

题目大意:

给出N个正整数a[1..N],再给出一个正整数k,现在可以进行如下操作:每次选择一个大于k的正整数a[i],将a[i]减去1,选择a[i-1]或a[i+1]中的一个加上1。经过一定次数的操作后,问最大能够选出多长的一个连续子序列,使得这个子序列的每个数都不小于k。M组询问,每组给出一个k

n<=1000000,m<=50,xi,k<=1e8

分析:

乍一看,有一些选择性,还有序列,像是dp,但是对于动规,因为可以经过无数次操作,而且数的范围也不小,完全没有想法。

所以这个时候,我们要再分析一下题意。

发现,无论怎么加减,这个序列的总和是不变的。而且,我们可以随意地进行无数次加减,可以将所有的高出k的数都可以填到低于k的数。

又发现,最终的合法子序列,就是这个序列区间内,每个数都要大于等于k,也就是说,这个区间的平均值必须大于等于k。

所以,我们用前缀和sum[i],表示原数列中前i个元素的和。

这样,我们就相当于枚举点对i,j,j<=i,使得在sum[i]-sum[j]>=k*(i-j)的情况下,i-j最大。(这是等价的,因为无论如何,我们会有一个子区间内的总和是不变的。所以可以采用原始的序列进行操作)

又,为了避免麻烦,我们可以将所有的数都减去k,这样就转化成了,找到最大的i-j,满足sum[i]-sum[j]>=0;

思路一:

枚举i,j点对。复杂度O(m*n^2) 直接慢的飞起。

思路二:

考虑优化,线性的东西,往往要考虑单调栈,或者是单调队列。

发现,对于一个枚举的右端点i,我们要枚举左端点1~i,之后还要重复枚举,只为了取max。是否有单调性?

发现,我们这里要考虑两个方面,一个是sum[i]-sum[j]>=0 一个是:i-j最大。所以从左端点的位置,和它的sum值关系入手。

发现,当k<j,且sum[k]<sum[j]时,k作为左端点,一定比j作为左端点更优,这样的j就可以扔掉。

于是,我们可以维护一个单调栈,正序加入,它从栈低到栈顶记录可能成为答案的左端点编号,从下往上,编号大小递增,sum值递减。

这样对于一个新枚举的i,我们要找到sum[i]-sum[j]>=0且最小的j,栈内二分即可。

复杂度O(m*n*logn) 可惜还是差一点。

思路三:

继续优化思路二。m、n看来是不太好优化了,logn的二分是否可以去掉呢?

发现,即使思路二用了单调栈,但是我们有的时候,还是会用一些“其实一眼就可以看出来的”不能可能成为答案的右端点来进行二分。

什么意思呢?

我们必须考虑右端点枚举的方式,是否也有同样的单调性??

发现,如果,j<i,并且sum[j]<sum[i],那么,j肯定也不会参与答案。

嗯,,,两个单调性,,,

我们可以开两个栈!!

除了上一个栈,我们还可以开一个储存可能成为右端点的栈。

其中,这个栈的更新,正序循环预处理。和上一个栈一样,从下到上,编号单增,sum单减。

我们同理,正序循环预处理第一个左端点栈。

然后进行答案选择,取出右端点的栈顶,不断pop左栈,直到sum[sta1[top1]]>sum[sta2[top2]],刚才最后pop的元素就可能成为答案。

至于为什么能pop,因为后面的右栈中的sum越来越大,而要使左端点越小越好,所以它们一定能过sum小的那些关,就直接跳过了。

思路四:

其实不用那么麻烦开两个栈。

我们初始化左端点栈之后,倒序循环右端点,同上不断pop,最后pop掉的左端点就用来尝试更新答案。

每次当sum[右端点]是历史最大值的时候才操作。

为什么是对的?

因为,右端点即将变小,小了之后,必须sum更大才有可能继续突破更大的sum[左端点],,否则,i小了,sum不增大,答案必然不优。

这样就避免了两个栈的麻烦。

注意:

开long long

代码:(代码好抄,思维难移)

#include<bits/stdc++.h>
using namespace std;
const int N=1000000+10;
const int M=55;
typedef long long ll;
int a[N];
ll sum[N];
int top,sta[N];
int n,m;
ll aver;
void work(int k)
{
    top=0;
    for(int i=1;i<=n;i++)
    {
        sum[i]=sum[i-1]+a[i]-k;
        if(!top||sum[sta[top]]>sum[i]){
            sta[++top]=i;
        }
    }//预处理左端点
    ll ans=0,mx=-2e18-1;
    for(int i=n;i>=1;i--){
        if(sum[i]>=0){
            ans=max(ans,(ll)i);
        }
        if(sum[i]>mx){
        for(;top&&sum[i]-sum[sta[top]]>=0;top--){
            ans=max(ans,(ll)i-sta[top]);
        }
         mx=sum[i];
        }
    }//倒序枚举右端点,尝试产生答案
    printf("%lld ",ans);
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]),aver+=a[i];
    }
    aver/=n;
    int k;
    for(int i=1;i<=m;i++){
        scanf("%d",&k);
        if(k<=aver){
            printf("%d ",n);continue;
        }//这里是个优化,如果k<=全集区间平均值,那么答案必然是n
        work(k);
    }
}

总结:

1.对于一个题目,我们应该去先想这是什么类型的题,但是更关键的是,挖掘题目中的隐含信息,比如这个题的平均值。可谓最厉害的算法,就是对症下药。

2.对于线性问题的优化,可以想到的有:单调队列,单调栈,斜率优化,分块,莫队,决策单调性(不会),四边形不等式(??),可行性dp转化为最优性dp,前缀和也算。

3.如果一步优化不完,一般不要轻易放弃这个思路,毕竟好题不是那么直白。考虑针对复杂度的哪个局限部位进行优化。继续挖掘信息。

4.尽量深刻理解算法本质,比如这个题,就很好的用到了两次单调栈的特点。

原文地址:https://www.cnblogs.com/Miracevin/p/9097901.html

时间: 2024-10-13 09:34:04

[POI2010]KLO-Blocks——一道值得思考的题的相关文章

一道有趣的算法题。。。

题目意思: 用1, 2, 3 ,4 ,5, 6, 7, 8, 9 组成3个三位数 abc, def 和 ghi, 每个数字恰好使用一次,要求abc:def:ghi = 1:2:3.输出所有解. 分析: 模拟所有三位数,判断条件有二: 一.i(abc):j(def):k(ghi)=1:2:3 二.判断是否出现的1~9之间的所有数字 代码: /** *一道有趣的算法题 * */ #include<iostream> #include<cstdio> using namespace st

一道模板元编程题源码解答(replace_type)

今天有一同学在群上聊到一个比较好玩的题目(本人看书不多,后面才知是<C++模板元编程>第二章里面的一道习题), 我也抱着试一试的态度去完成它, 这道题也体现了c++模板元编程的基础和精髓: 类型就是数据. 题目如下所述: Write a ternary metafunction replace_type<c,x,y> that takes an arbitrary compound type c as its first parameter, and replaces all oc

一个值得思考的例子

# -*- coding: cp936 -*- #python 27 #xiaodeng #一个值得思考的例子 def intersect(seq1,seq2): res=[] #初始化 for x in seq1: #遍历seq1 if x in seq2: # res.append(x) return res #注意return和res=[]的同等缩进 seq1=[4,5,1,2,3] seq2=[1,2,3] print intersect(seq1,seq2) 上面的例子也有多态的体现,

一道有趣的算法题:仿照Excel的列编号,给定一个数字,输出该列编号字符串

       By Long Luo 最近遇到一个算法题: 仿照Excel的列编号,给出一个数字,输出该列编号字符串. 例如:A对应1,Z对应26,AA对应27,AZ对应52 ...... 这个题目是一个典型的26进制思路去处理,但是这个题目里面有很多陷阱,在1, 26, 52等特殊情况进行考虑,经过晚上接近1个小时的编写,完成的代码如下: C++代码如下: #include <iostream> #include <string.h> using namespace std; /

一道简单的数据结构题 栈的使用(括号配对)

一道简单的数据结构题 发布时间: 2017年6月3日 18:46   最后更新: 2017年6月3日 18:51   时间限制: 1000ms   内存限制: 128M 描述 如果插入"+"和"1"到一个括号序列,我们能得到一个正确的数学表达式,我们就认为这个括号序列是合法的.例如,序列"(())()", "()"和"(()(()))"是合法的,但是")(", "(()&quo

从一道简单的dp题中学到的...

今天想学点动态规划的知识,于是就看了杭电的课件,数塔问题啊,LCS啊都是比较经典的动规了,然后随便看了看就开始做课后练习题... HDOJ 1421 搬寝室 http://acm.hdu.edu.cn/showproblem.php?pid=1421 题目大意:从n(n <= 2000)个数中选出k对数(即2*k个),使它们的差的平方和最小. 例如:从8,1,10,9,9中选出2对数,要使差的平方和最小,则应该选8和9.9和10,这样最小,结果为2 因为知道是dp的题,先建个dp[][]数组,然

值得一做》关于一道DP+SPFA的题 BZOJ1003 (BZOJ第一页计划) (easy+)

这是一道数据范围和评测时间水的可怕的题,只是思路有点难想,BUT假如你的思路清晰,完全了解怎么该做,那就算你写一个反LLL和反SLE都能A,如此水的一道题,你不心动吗? 下面贴出题目 Description 物流公司要把一批货物从码头A运到码头B.由于货物量比较大,需要n天才能运完.货物运输过程中一般要转停好几个码头.物流公司通常会设计一条固定的运输路线,以便对整个运输过程实施严格的管理和跟踪.由于各种因素的存在,有的时候某个码头会无法装卸货物.这时候就必须修改运输路线,让货物能够按时到达目的地

算法题:四则运算(中缀式到后缀式的转换,值得思考的逆波兰式)

/* 字符串的四则运算.给出一个字符串, 包含0~9的数字和 + -*\/ ()的运算符, - 仅代表减号不代表负数.举例如下: 输入:1 + 2 * (3 - 4) */ //哈哈,看到这道题,其实一点也不难,这个题根本就不用思考, //当然是你明白算法之后,这里要用到的算法是逆波兰式. //如果你有不明白的地方,可以上网搜逆波兰式. /* 我的总结:计算机无法理解人类的正向思维,于是为了满足计算机的 思维,我们会反其道而行之,将操作符号放在操作数的后面,形成后缀 表达式,但是如果你能按百科上

一些值得思考的&quot;小题&quot;一

如下是我们查找数组中某个元素的一种通常做法 const int *Find(const int *array, int length, int x) { const int *p = array; for (size_t i = 0; i < length; i++) { if (*p == x) { return p; } p++; } return 0; } 在这种方法下,我们必须保证数组为int型,知道数组的长度,甚至还要确定整的存在这样一个数组.请你利用template重写此方法来消除所