資料結構優化貪心
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\)
有了大體思路框架之後,開始處理實現細節問題:
-
使用單調佇列維護\(n-k+1\)個數中的最大值,這樣可以保障我們能夠選出\(k\)個數
-
先把前\(n-k\)個數入隊,然後每入隊一個元素,統計一次答案,時間複雜度\(O(n)\)
-
題目中要求每個數最多選\(1\)次,所以在維護單調佇列時,每記錄一次最大值當作答案,隊頭就要彈出
-
原題是一道互動題,需要下載一個互動庫,使用前注意變數名不要和互動庫中的變數名重複(互動庫會和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改出來的……
//淦,年代久遠我看不太懂自己的程式碼了,所以註釋可能有鍋
}