贪心优化
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)\)的算法维护最大值,想到单调队列维护
有了大体思路框架之后,开始处理实现细节问题:
-
使用单调队列维护\(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
#include
#include
#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
洛谷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}\),直到极差\(,此时\(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
#include
#include
#include
#include
#include
#include
洛谷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_j\)无法满足,那么满足\(b_j\)一定不会比满足\(b_i\)更差
它的证明也很简单
如果满足了\(b_i\),那么需要花费\(b_i\),答案\(ans+1\),如果满足\(b_j\),花费\(b_j(b_j,同样使得\(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
#include
#include
#include
#include
#include
#include
洛谷P3419 [POI2005]SAM-Toy Cars
标签与传送门
传送门:洛谷P3419 [POI2005]SAM-Toy Cars
标签:二叉堆优化贪心
题意梳理
在地上能存放\(k\)个物品,每个物品都可能在某个时刻被需要,如果此时这个物品在架子上,那么答案+1,把这个物品放到地上,从地上扔掉另一个物品到架子上
要求最小化答案
\(Solution\)
不难发现,此题的主要决策就是当地上已经存在了\(k\)个物品时,有一个新的物品要放到地上,该把地上的哪个物品放回到架子上
口胡简单的思路:把下一次被需要的时间最靠后的物品放到架子上
这里因为本人做题的时候也是一眼口胡的这个思路,并没有严谨证明,翻翻你谷题解区,大部分dalao都是提供各种奇怪思路,好像都没有严谨证明的……
所以这里挖一个坑,等本人闲的没事干了,补上证明
\(Code\)
#include
#include
#include
#include
#include
#include
#include
今天的分享就到这里,感谢您的阅读,给个三连球球辣!OvO
相关