1. 程式人生 > 實用技巧 >資料結構優化貪心

資料結構優化貪心

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\)次,就可以保證字典序最大

本題資料較大,\(n\leq 1.5\times 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\n", 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\n", 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\)

首先急需解決的是這兩個最值怎麼求、用什麼資料結構維護的問題

資料範圍\(n\leq 3\times 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\n",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_i\leq 10^9\),顯然揹包會空間、時間爆炸,\(n\leq 2.5\times 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)\),對於\(n\leq 2.5\times 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\n",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); }

int n,k,p,fl[maxn],play[maxn*5],ans;//地上玩具編號,玩玩具編號序 

std::vector<int>toy[maxn];//編號為i的玩具在第i回合被需求 

int ids[maxn];//toy[]的模擬指標 

std::priority_queue< std::pair<int,int> >Q;//宣告一個大根堆
int main(){
	n=read(),k=read(),p=read();//輸入玩具數量n,地板容量k,玩玩具序列長度p
	for(int i=1;i<=p;i++){
		play[i]=read();//讀入在i秒要玩play[i]的玩具
		toy[play[i]].push_back(i);//play[i]的玩具在第i回合要玩
	}
	for(int i=1;i<=p;i++){//讀入玩玩具序列
		if(k>0){//地板容量>0時
			if(ids[play[i]]+1!=toy[play[i]].size()){//如果這個玩具在之後還要玩
				ids[play[i]]++;//第play[i]種玩具下次玩是toy[play[i]][ids[play[i]]]
				Q.push(std::make_pair(toy[play[i]][ids[play[i]]],play[i]));//入堆
			}else Q.push(std::make_pair(0x3f3f3f3f,play[i]));//下次不玩了,給一個極大值
			if(fl[play[i]]==1) continue;//這個玩具已經在地板上,就不用拿了
			fl[play[i]]=1;//從架子上拿下來了
			k--;//地板容量--
			ans++;//次數++
			continue;
		}
		if(fl[play[i]]==1){//已經在地板上,那麼需要更新下一次什麼時候玩
			if(ids[play[i]]+1!=toy[play[i]].size()){
				ids[play[i]]++;
				Q.push(std::make_pair(toy[play[i]][ids[play[i]]],play[i]));//下次還玩,同上更新
			}else Q.push(std::make_pair(0x3f3f3f3f,play[i]));//下次不玩了,給個極大值
		}else{
			fl[Q.top().second]=0;//堆頂要被放回去,那麼標記堆頂不在地板上
			Q.pop();//彈出
			if(ids[play[i]]+1!=toy[play[i]].size()){//下次還玩,同上更新
				ids[play[i]]++;
				Q.push(std::make_pair(toy[play[i]][ids[play[i]]],play[i]));
			}else Q.push(std::make_pair(0x3f3f3f3f,play[i]));//下次不玩了,給個極大值
			fl[play[i]]=1;//標記現在play[i]在地板上
			ans++;//次數++
		}
	}
	printf("%d\n",ans);//輸出次數
	return 0;
      //淦,我是怎麼寫出這麼陰間的程式碼的……我是怎麼把這個bug改出來的……
      //淦,年代久遠我看不太懂自己的程式碼了,所以註釋可能有鍋	
}

今天的分享就到這裡,感謝您的閱讀,給個三連球球辣!OvO