• 数据结构优化贪心


    Part 1:关于贪心与数据结构

    一个贪心算法的本质是:不断做出当前情况的最优解,最终可以得到全局最优解

    只要这个问题的阶段决策满足上述要求,就可以使用贪心法求解

    所以,我们要使得当前阶段决策最优,通常会用到“最值”,即可做出的选择中,最好的那一个

    于是,数据结构应运而生,它可以很好的帮助我们维护一堆数据的某个性质,从而有效提高程序的运行效率

    (这才是数据结构诞生的意义,所以暴力数据结构题都是毒瘤)

    常用于优化其他算法的数据结构:单调队列、单调栈、优先队列、树状数组(线段树)等等

    Part 2:例题整理

    洛谷P3487[POI2009]ARC-Architects

    标签与传送门

    传送门:洛谷P3487[POI2009]ARC-Architects

    标签:单调队列优化贪心

    题意梳理

    一句话来说:在整个长度为(n)的序列中选出长度为(k)子序列,使得这(k)个数构成的序列的字典序最大

    (Solution)

    考虑如何最大化字典序,分析字典序的性质——越靠前的数越大,那么这个字典序就越大

    比如两个序列:(a=[2,3000,1000],b=[3,1,1]),虽然(a)的第二、三项很大,但是因为第一项(2<3),所以排列(a)的字典序小于排列(b)的字典序

    那么贪心的想:只要从第一个依次向后选,并且每次保证进来的数最大,这样贪心的选(k)次,就可以保证字典序最大

    本题数据较大,(nleq 1.5 imes 10^7),需要使用(O(n))的算法维护最大值,想到单调队列维护

    有了大体思路框架之后,开始处理实现细节问题:

    1. 使用单调队列维护(n-k+1)个数中的最大值,这样可以保障我们能够选出(k)个数

    2. 先把前(n-k)个数入队,然后每入队一个元素,统计一次答案,时间复杂度(O(n))

    3. 题目中要求每个数最多选(1)次,所以在维护单调队列时,每记录一次最大值当作答案,队头就要弹出

    4. 原题是一道交互题,需要下载一个交互库,使用前注意变量名不要和交互库中的变量名重复(交互库会和AC代码一起给出)

    (Code)

    /*************************************************************************}
    {*                                                                       *}
    {*                     XVI Olimpiada Informatyczna                       *}
    {*                                                                       *}
    {*   Zadanie: Architekci (ARC)                                           *}
    {*   Plik:    carclib.c                                                  *}
    {*   Autor:   Bartosz Gorski                                             *}
    {*   Opis:    Biblioteka do wczytywania danych wejsciowych i wypisywania *}
    {*            wyniku                                                     *}
    {*                                                                       *}
    {*************************************************************************/
    
    #include <stdlib.h>
    #include <stdio.h>
    #include <time.h>
    
    #define MAGIC_BEGIN -435634223
    #define MAGIC_END -324556462
    
    #define MIN_K 1
    #define MAX_K 1000000
    #define MAX_N 15000000
    #define MIN_A 1
    #define MAX_A 1000000000
    #define MIN_TYP 1
    #define MAX_TYP 3
    #define MIN_PAR 0
    #define MAX_PAR 1000000000
    
    #define ERROR 0
    #define CORRECT 1
    
    #define unlikely(x) __builtin_expect(!!(x), 0)
    
    static int init = 0; // czy zostala juz wywolana funkcja inicjuj()
    static int lib_n; // ile biblioteka podala juz liczb
    static int con_k; // ile zawodnik podal liczb
    
    
    static int N, K, A, TYP, PAR; // parametry testu wczytywane z pliku
    static int bre, len_sub, bou, is_end; // zmienne pomocnicze
    
    static int rand2_status = 198402041;
    
    static inline int rand2(int a, int b){
      rand2_status = rand2_status * 1103515245 + 12345;
      int x = rand2_status;
      if (x < 0) x = -x; // -2^31 sie nie zdarza :D
      x >>= 1;
      x = a + x % (b - a + 1);
      return x;
    }
    
    /* test losowy */
    static inline int random_test()
    {
        return rand2(1, A);
    }
    
    /* test z dlugim podciagiem nierosnacym */
    static inline int decreasing_test()
    {
        int tmp;
        if(bre == 0) {
            bre = rand2(0, (N - lib_n + 1 - len_sub));
            tmp = A;
            A -= rand2(0, (A - 1) / len_sub);
            len_sub--;
        }
        else {
            bre--;
            tmp = rand2(1, A);
        }
        return tmp;
    }
    
    /* test z dlugim podciagiem niemalejacym */
    static inline int increasing_test()
    {
        return bou - decreasing_test();
    }
    
    static void finish(int res, char *com)
    {
        if(res == ERROR)
            printf("%s
    ", com);
        exit(0);
    }
    
    /* Inicjuje dane wejsciowe i zwraca liczbe projektow */
    int inicjuj()
    {
        if(init == 1)
            finish(ERROR, "Program zawodnika moze wywolac funkcje inicjuj tylko raz!!!");
        init = 1;
        scanf("%d", &K);
        if (K > 0){
          TYP = 0;
          N = MAX_N + 2;
          return K;
        }
        int magic_begin, magic_end;
        scanf("%d%d", &magic_begin, &TYP);
        if(magic_begin != MAGIC_BEGIN || TYP < MIN_TYP || TYP > MAX_TYP)
            finish(ERROR, "Program zawodnika nie moze korzystac z stdin!!!");
        scanf("%d%d%d%d", &N, &K, &A, &PAR);
        if(N < 1 || N > MAX_N || N < K || K > MAX_K || A < MIN_A || A > MAX_A 
            || PAR < MIN_PAR || PAR > MAX_PAR)
            finish(ERROR, "Program zawodnika nie moze korzystac z stdin!!!");
        scanf("%d", &magic_end);
        if(magic_end != MAGIC_END)
            finish(ERROR, "Program zawodnika nie moze korzystac z stdin!!!");
        con_k = 0;
        lib_n = 0;
        is_end = 0;
        if(TYP == 2 || TYP == 3) {
            len_sub = PAR;
            bre = 0;
        }
        if(TYP == 2)
            bou = A--;
        return K;
    }
    
    /* Sluzy do wczytania ciagu reprezentujacego jakosci projektow */
    int wczytaj()
    {
        if(unlikely(init == 0))
            finish(ERROR, "Program zawodnika nie wywolal funkcji inicjuj!!!");
        if(unlikely(lib_n > N || is_end == 1))
            finish(ERROR, "Program zawodnika wywolal funkcje wczytaj po otrzymaniu informacji o koncu ciagu!!!");
        if(unlikely(lib_n == N))
            return 0;
        lib_n++;
        switch (TYP) {
          case 0:
            scanf("%d", &A);
            if(A == 0)
              is_end = 1;
            return A;
            break;
          case 1: return random_test(); break;
          case 2: return increasing_test(); break;
          case 3: return decreasing_test(); break;
          default:
                  finish(ERROR, "Nieznany typ testu");
        }
        return -1;
    }
    
    /* Sluzy do wypisania wyznaczonego podciagu */
    void wypisz(int jakoscProjektu)
    {
        if(init == 0)
            finish(ERROR, "Program zawodnika nie wywolal funkcji inicjuj!!!");
        printf("%d
    ", jakoscProjektu);
        if(++con_k == K)
            finish(CORRECT, "");
    }
    //以上均是交互库内容
    #define maxn 15000010
    #define inf 0x3f3f3f3f
    
    struct Queue{
    	int und,num;//建立结构体,存储下标和数字大小
    }q[maxn];//声明单调队列q
    
    int main(){
    	int a[maxn],n,k;
    	int ans[maxn],it;
    	k=inicjuj();
    	for(int i=1;;i++){
    		a[i]=wczytaj();//按要求读入
    		if(a[i]==0) break;
    		n++;//n是元素个数 
    	}
    	k=n-k+1;
    	int i,head=1,tial=0;//建立单调队列维护k个数中最大值 
    	for(i=1;i<k;i++){
    		while(head<=tial&&q[tial].num<a[i]) tial--;
    		q[++tial].und=i,q[tial].num=a[i];
    	}//先入队n-k个元素
    	for(;i<=n;i++){
    		while(head<=tial&&q[tial].num<a[i]) tial--;
    		q[++tial].und=i,q[tial].num=a[i];
    		while(q[head].und<i-k+1) head++;//新入队元素,维护单调队列性质
    		ans[it++]=q[head].num;//更新答案
    		head++;//弹出队头
    	}
    	for(int i=0;i<it;i++)
    		wypisz(ans[i]);//写出答案
    	return 0;
    } 
    

    洛谷P3512[POI2010]PIL-Pilots

    标签与传送门

    传送门:洛谷P3512[POI2010]PIL-Pilots

    标签:单调队列优化贪心

    题意梳理

    给定一个序列(S)和常数(k),输出连续且极差不超过(k)的最长子序列长度

    这里为什么要把连续标出来呢?因为你谷里有些题解中说的是“不连续”,所以这里请大家注意

    如果仔细阅读一下英文题面的话,就会发现给出的一句话中文翻译并不十分准确:

    上面这一大段英文的意思大概是:

    你的任务是编写一个程序,对于给定长度的位置测量序列,求出在位置容差范围内的最长飞行片段的长度

    显然所求序列应该是连续的

    (Solution)

    首先急需解决的是这两个最值怎么求、用什么数据结构维护的问题

    数据范围(nleq 3 imes 10^6),猜测正解大概是一个(O(n))的算法。那么,单调队列当之无愧

    先口胡一个随便就能想出来的玄学贪心思路:

    从第一个元素扫描整个序列,建立两个单调队列,其中(q_1)维护最大值,(q_2)维护最小值
    设一个可能构成答案的序列(a)中的第一个元素是(S_i),当扫描到(S_j)时,向两个单调队列里加入(S_j),检查极差是否大于(k)
    若不大于(k),那么这个串的长度就是(j-i+1),用这个(j-i+1)更新答案(ans)
    如果大于(k),那么需要不停淘汰(a_{max})的或者(a_{min}),直到极差(<k),此时(i)变为去掉的极值的下标(+1)

    尝试证明贪心:

    Case 1:
    如果现在正在统计长度的序列(a)加上下一个数(x),极差不超过(k),那么把(x)计入长度,一定不会使得结果变差(不拿白不拿法证明贪心)
    因为如果不把(x)计入(a),根据连续性的要求,(x)后面的元素都不能计入(a)的长度,所以不加(x)最少要比加入(x)得到长度少(1)
    又因为此题中每一个可能构成答案的序列都是独立的,也就是说,在这个序列中选不选(x)对其他序列长度没有影响
    那既然不选会亏,选了又没有任何可能导致错误的后果,那为什么不选(x)呢?选它!
    Case 2:
    正在统计长度的序列(a)加上下一个数(x)之后,极差超过了(k)的情况
    假设序列(a)的第一个元素是(S_i),当前枚举到的元素是(S_j)(q_1)维护最大值,队头下标(d_1)(q_2)维护最小值,队头下标(d_2)
    对于(S_j)与队列中的极大值或极小值冲突,需要弹出队中的极大值或者极小值,使得极差小于(k)
    显然,对于越靠前的极值,越难以满足之后的决策,所以,当不满足条件时,优先弹出下标较小的极值
    弹出后得到新的满足条件的序列的开始元素下标(i),是被弹出的下标最大的极值的下标(+1)

    (Code)

    #include<cstdio>
    #include<cstring>
    #include<queue>
    #include<stack>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<utility>
    #include<iostream>
    #include<list>
    #include<ctime>
    #include<cmath>
    #include<cstdlib>
    #include<iomanip>
    typedef long long int ll;
    
    inline int read(){
    	int fh=1,x=0;
    	char ch=getchar();
    	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
    	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
    	return fh*x;
    }
    
    inline int _abs(const int x){ return x>=0?x:-x; }
    
    inline int _max(const int x,const int y){ return x>=y?x:y; }
    
    inline int _min(const int x,const int y){ return x<=y?x:y; }
    
    inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
    
    inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
    
    //2147483647 
    const int maxn=3000005;
    const int inf=0x3f3f3f3f;
    
    struct Queue{
    	int und,num;
    };//结构体,分别记录下标和值
    
    Queue q1[maxn],q2[maxn];
    
    int a[maxn],k,n,ans;
    int main(){
    	k=read(),n=read();
    	for(int i=1;i<=n;i++)
    		a[i]=read();
    	int head=1,tial=0,front=1,back=0,st=1;//初始化队头队尾
    	for(int i=1;i<=n;i++){
    		while(head<=tial&&q1[tial].num<a[i]) tial--;
    		q1[++tial].num=a[i],q1[tial].und=i;//维护单调递增队列 
    		
    		while(front<=back&&q2[back].num>a[i]) back--;
    		q2[++back].num=a[i],q2[back].und=i;//维护单调递减队列
    		
    		while(_abs(q1[head].num-q2[front].num)>k){//极值大于k
    			if(q1[head].und<q2[front].und){//找到下标较小的极值,弹出
    				st=q1[head].und+1;//更新答案序列开始的元素下标st
    				head++;
    			}else{
    				st=q2[front].und+1;
    				front++;
    			}
    		}
    		
    		ans=_max(ans,i-st+1);//更新答案,看看答案是不是变长了
    	}
    	printf("%d
    ",ans);//输出答案
    	return 0;
    } 
    

    洛谷P3545 [POI2012]HUR-Warehouse Store

    标签与传送门

    传送门:洛谷P3545 [POI2012]HUR-Warehouse Store

    标签:二叉堆优化贪心

    题意梳理

    给定一个初始为0的(x)(x)每天(+a_i),然后可以选择以(b_i)的代价,使得答案(+1),也可以什么都不做,问答案最大是多少

    (Solution)

    本题第一眼看上去像一个01背包,但是仔细想想,(a_i,b_ileq 10^9),显然背包会空间、时间爆炸,(nleq 2.5 imes 10^5)的数据,搜索也吃不消

    开始往贪心上想,先大胆口胡一个思路:

    对于每一个(b_i),若此时(x>b_i),那么满足这个(b_i)

    显然它是错的,并且很容易构造出数据hack掉这个思路:

    3
    100 0 0 
    100 50 50
    

    比如这组数据,在(i=1)时,我们满足了(b_1=100),但是后面两个(50)就无法满足,此时程序答案是(1),正确答案是(2)

    那么这个思路就没有一点可取之处吗?经过一番思索后,发现它其实对正解有一定的启发(也就是朝着正确的方向犯错)

    上面思路错误之处就在于:对于一个前面的(b_i)可能很大,导致满足了前面的(b_i)之后,后面的一些较小的(b_j(j>i))无法满足

    其实不难证明这样一个规律

    对于一个已经被满足的(b_i),若存在(b_j<b_i)(b_j)无法满足,那么满足(b_j)一定不会比满足(b_i)更差

    它的证明也很简单

    如果满足了(b_i),那么需要花费(b_i),答案(ans+1),如果满足(b_j),花费(b_j(b_j<b_i)),同样使得(ans+1),而(x)在第(j)天增加了(b_i-b_j>0),所以更优

    那么我们需要给我们的贪心一个“反悔”的机会,即满足不了(b_j),看之前有没有满足比(b_j)大的(b_i),如果有,那么选择在第(i)天不满足(b_i),而在第(j)天满足(b_j)

    然后剩余存货(x)加上(b_i-b_j),继续按照“能满足则满足,满足不了,能反悔就反悔”的规则向后扫描,直到扫描完整个数组,得到答案(ans)即为最大值

    我们若扫描已经满足的(b_i)来找到大于(b_j)的元素的话,最坏复杂度达到了(O(n^2)),对于(nleq 2.5 imes 10^5)的数据,显然无法通过此题

    所以最后一点,就是需要搞一个数据结构来维护它,显然二叉堆可以方便的维护最大值,我们可以把已经满足的(b_i)都扔到二叉堆里,需要“反悔”的时候,取出最大值即可

    这样复杂度降低到(O(nlogn))

    (Code)

    #include<cstdio>
    #include<cstring>
    #include<queue>
    #include<stack>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<utility>
    #include<iostream>
    #include<list>
    #include<ctime>
    #include<cmath>
    #include<cstdlib>
    #include<iomanip>
    typedef long long int ll;
    
    inline ll read(){
    	ll fh=1,x=0;
    	char ch=getchar();
    	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
    	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
    	return fh*x;
    }
    
    inline int _abs(const int x){ return x>=0?x:-x; }
    
    inline int _max(const int x,const int y){ return x>=y?x:y; }
    
    inline int _min(const int x,const int y){ return x<=y?x:y; }
    
    inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
    
    inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
    
    const int maxn=250010;
    const int inf=0x3f3f3f3f;
    
    struct Node{
    	ll udn,num;//下标,货物数量 
    };
    bool operator < (const Node a,const Node b){ return a.num<b.num; }
    std::priority_queue<Node>Q;
    
    ll n,k;
    ll in[maxn],out[maxn];//记录进货和需求(a_i和b_i)
    bool vis[maxn];//表示b_i有没有被满足
    
    int main(){
    	n=read();
    	for(int i=1;i<=n;i++)
    		in[i]=read();
    	for(int i=1;i<=n;i++)
    		out[i]=read();
    	for(int i=1;i<=n;i++){//扫描到第i天
    		k+=in[i];
    		if(k>=out[i]){//库存大于b_i
    			Q.push((Node){i,out[i]});//满足了,扔到二叉堆里
    			vis[i]=true;//第i天的需求满足 
    			k-=out[i];//减少库存量 
    		}else if(Q.size()!=0&&Q.top().num>out[i]){
    			vis[Q.top().udn]=false;//在之前花费最大的那天反悔 
    			k+=Q.top().num;//要回那天的花费 
    			Q.pop();//那天被反悔了弹出 
    			k-=out[i];//今天满足要求 
    			vis[i]=true;
    			Q.push((Node){i,out[i]}); //今天满足了,扔到二叉堆里,因为今天在之后的决定里还有可能被反悔掉
    		}
    	}
    	int ans=0;
    	for(int i=1;i<=n;i++)
    		if(vis[i]) ans++;//扫一遍(自带n的常数),看看满足了几天
    	printf("%d
    ",ans);
    	for(int i=1;i<=n;i++)
    		if(vis[i]) printf("%d ",i);//第i天满足了
    	return 0;
    }
    

    洛谷P3419 [POI2005]SAM-Toy Cars

    标签与传送门

    传送门:洛谷P3419 [POI2005]SAM-Toy Cars

    标签:二叉堆优化贪心

    题意梳理

    在地上能存放(k)个物品,每个物品都可能在某个时刻被需要,如果此时这个物品在架子上,那么答案+1,把这个物品放到地上,从地上扔掉另一个物品到架子上

    要求最小化答案

    (Solution)

    不难发现,此题的主要决策就是当地上已经存在了(k)个物品时,有一个新的物品要放到地上,该把地上的哪个物品放回到架子上

    口胡简单的思路:把下一次被需要的时间最靠后的物品放到架子上

    这里因为本人做题的时候也是一眼口胡的这个思路,并没有严谨证明,翻翻你谷题解区,大部分dalao都是提供各种奇怪思路,好像都没有严谨证明的……

    所以这里挖一个坑,等本人闲的没事干了,补上证明

    (Code)

    #include<cstdio>
    #include<cstring>
    #include<queue>
    #include<stack>
    #include<algorithm>
    #include<set>
    #include<map>
    #include<utility>
    #include<iostream>
    #include<list>
    #include<ctime>
    #include<cmath>
    #include<cstdlib>
    const int maxn=100005; 
    inline int read(){
    	int fh=1,x=0;
    	char ch=getchar();
    	while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
    	while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
    	return fh*x;
    }
    inline int _abs(const int x){ return x>=0?x:-x; }
    inline int _max(const int x,const int y){ return x>=y?x:y; }
    inline int _min(const int x,const int y){ return x<=y?x:y; }
    inline int _gcd(const int x,const int y){ return y?_gcd(y,x%y):x; }
    inline int _lcm(const int x,const int y){ return x*y/_gcd(x,y); }
    
    #define maxn 100005
    #define inf 0x3f3f3f3f
    
    using namespace std;
    
    int n,k,p,ans;
    int a[maxn*5];
    bool in_floor[maxn];//记录是否在地板上 
    int it[maxn];//vector的模拟指针 
    
    vector<int>v[maxn];//记录每个玩具在第几回合要玩
    
    struct Toy{
    	int nxt,num;//num是当前玩具编号,nxt是下次在第几回合玩,如果不再玩了,那么赋值inf 
    }; 
    bool operator < (const Toy &a,const Toy &b){
    	return a.nxt<b.nxt;
    }
    priority_queue<Toy>Q;
    
    int main(){
    	n=read(),k=read(),p=read();//n是玩具种类,k是地板容量,p是需求序列 
    	for(int i=0;i<p;i++){
    		a[i]=read();//读入玩玩具的序列 
    		v[a[i]].push_back(i);//编号为a[i]的玩具在第i回合被需求了 
    	}
    
    	for(int i=0;i<p;i++){//扫一遍整个序列
    		if(in_floor[a[i]]){//如果a[i]在地板上
    			
    			it[a[i]]++;//指针指向下一次需求
    			
    			if(it[a[i]]<v[a[i]].size())//如果之后还要玩,入堆下一次要玩的时间v[a[i]][it[a[i]]],和编号a[i]
    				Q.push((Toy){v[a[i]][it[a[i]]],a[i]});
    			else Q.push((Toy){inf,a[i]});//下次不玩了,时间记为inf,保证下次一定会被优先弹出
    			
    			continue;
    		}
    		if(k>0){//地板上有空的位置
    			in_floor[a[i]]=true;//在地板上
    			
    			it[a[i]]++;//指针指向下一个需求
    			
    			if(it[a[i]]<v[a[i]].size())
    				Q.push((Toy){v[a[i]][it[a[i]]],a[i]});
    			else Q.push((Toy){inf,a[i]});//同上
    			
    			k--;ans++;//地板容量--,答案++
    		}else{
    			while(!in_floor[Q.top().num]) Q.pop();//如果堆顶元素并不在地板上,弹出
    			
    			in_floor[Q.top().num]=false;//放回去
    			in_floor[a[i]]=true;//拿下新的来
    				
    			Q.pop();//弹出堆顶
    			it[a[i]]++;//新的指针更新
    			
    			if(it[a[i]]<v[a[i]].size())
    				Q.push((Toy){v[a[i]][it[a[i]]],a[i]});
    			else Q.push((Toy){inf,a[i]});//同上
    			
    			ans++;
    			continue;
    		}
    	}
    	printf("%d",ans);//输出答案
    	return 0;
    }
    //upd 2020/9/28优化了代码可读性与整齐度,增加了一些注释
    //双倍经验题SPOJ688过不了,仍然有锅,待调试
    

    今天的分享就到这里,感谢您的阅读,给个三连球球辣!OvO

  • 相关阅读:
    数学工具WZgrapher
    零线和地线的区别,示波器如何测量市电?
    使用直流稳压电源时的注意事项!
    中文全角和半角输入有什么区别?
    ThinkingRock:使用方法
    2014记首
    如何使用Excel绘制甘特图
    AStyle代码格式工具在source insight中的使用
    STM32F103系列命名规则
    上市公司行情查询站点
  • 原文地址:https://www.cnblogs.com/zaza-zt/p/13574823.html
Copyright © 2020-2023  润新知