贪心优化

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); }

#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

posted @ 2020-09-12 19:40  ZTer  阅读(274)  评论(0编辑  收藏  举报