【贪心-单调栈】中山纪念中学暑期游Day12——灵知的太阳信仰

前言

感觉自己就没弄懂过 单调栈\队列相关的优化... ...

题目

在炽热的核熔炉中,居住着一位少女,名为灵乌路空。
据说,从来没有人敢踏入过那个熔炉,因为人们畏缩于空所持有的力量——核能。
核焰,可融真金。
 
咳咳。
每次核融的时候,空都会选取一些原子,排成一列。然后,她会将原子序列分成一些段,并将每段进行一次核融。
一个原子有两个属性:质子数和中子数。
每一段需要满足以下条件:
1、同种元素会发生相互排斥,因此,同一段中不能存在两个质子数相同的原子。
2、核融时,空需要对一段原子加以防护,防护罩的数值等于这段中最大的中子数。换句话说,如果这段原子的中子数最大为x,那么空需要付出x的代价建立防护罩。求核融整个原子序列的最小代价和。

Input

第一行一个正整数N,表示原子的个数。
接下来N行,每行两个正整数pi和ni,表示第i个原子的质子数和中子数。

Output

输出一行一个整数,表示最小代价和。

Sample Input

5
3 11
2 13
1 12
2 9
3 13

Sample Output

26

【数据范围】

对于20%的数据,1<=n<=100
对于40%的数据,1<=n<=1000
对于100%的数据,1<=n<=10^5,1<=pi<=n,1<=ni<=2*10^4

分析

最开始自己以为是贪心(不是贪心至少也能骗到分吧),结果没想到暴零了.../暴风哭泣

参考题解\博客:https://blog.csdn.net/doyouseeman/article/details/53121206

这道题有一个很显然的dp方程,f[i]=f[j]+max(b[j+1…i])。 
我们设后面的为g,那么很明显g是递增的。 
那么用一个队列来维护后面的那个东西。 
因为要满足一段里面元素不重复,所以要求一个l[i]表示i往左最远扩展到的节点。 
那么每次,如果队首的位置小于l[i]那么head++,如果队尾的b小于等于当前的b[i],那么tail–(因为要像一个单调栈一样,有高的就把它踢掉)。(要保证当前这个队列里面所有的值都合法)。 
那么现在i就可以进队了。 
现在就有两种情况: 
1、这个队列只有一个数,就是刚刚加进来的那个数,那么就直接更改一下队列中的值就好了。 
2、这个队列中不止一个数,那么除了要更改刚刚进来的数的值,现在还要更新一下最小的值(用一个set来维护),现在的head因为l[i]不一样了,所以初始的高度可能会不一样,所以需要重新入队一次。

另一个JZOJ的题解:

首先来想最简单的动态规划,通过第一个数的限制,我们可以得到每个位置的状态可以从哪 些位置转移过来,得到如下形式的式子: f(i) = ????=?[?] ?−1 (?[? + 1][?] + ?[?]) 其中 s[i][j]表示从 i 到 j 的最大值,l[i]表示 i 位置最多向左走到哪里。 然后套路一波,用个单调递减的单调队列,每次将队头不合法的依次踢掉,然后我们来想想, 这样我们只是得到了可以转移过来的状态集,那么如何维护答案呢?

观察下图:

现在我们的 i 在位置 8 上,红色框表示 i 向左最多到达的位置(即 2 与 8 的 p[i]是一样的) 可以看出,除了队头之外,其他的在单调队列里的元素的贡献都是从前面一个位置的 f 转移 而来的(因为 f 是单调不减的),而队头的则是依赖于 l[i]的。 假设当前的队列为:a[head. .tail]且∀head ≤ i < tail, a[i] < a[i + 1] 那么当前点的答案为:max(????=ℎ???+1 ???? (ℎ[?[?]] + ?[?[? − 1]]) , h[a[head]] + f[l[i] − 1]) 观察到每次我们移动一个点是有许多没有变的状态的,每个元素进一次出一次,于是我们用 堆或者其他数据结构来维护队列中每个元素的贡献,对于队头特殊处理。 时间复杂度O(n ???2n)

考试贪心瞎搞代码

#include<cstdio>
#include<cmath>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;//记得算空间、开ll 
const int MAXN=1e5;
bool vis[MAXN+5];
int a[MAXN+5],b[MAXN+5];
int n;
ll ans,Max;
int main()
{
	//freopen("array.in","r",stdin);
	//freopen("array.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&a[i],&b[i]);
		//printf("%d vis:%d\n",a[i],vis[a[i]]);
		if(!vis[a[i]])//没有出现过 
		{
			vis[a[i]]=1;
			Max=max(Max,(ll)b[i]);
		}
		else//出现过 
		{
			ans+=Max;//累加前一段的答案 
			Max=b[i];
			memset(vis,0,sizeof(vis));
			vis[a[i]]=1;
		}
		//printf("Max:%d ans:%d\n",Max,ans);
	} 
	ans+=Max;//累加最后一段的答案 
	printf("%lld",ans);
	return 0;
}
/*
可行的时间复杂度:n or nlogn 
贪心策略:
1.大的数尽量去包含其他数
2. 遇到出现过的数就被迫分一段
3.还好自己出了些有用的数据,发现了有一些bug,汗...
*/

AC代码

#include<cstdio>
#include<set>
#include<cmath>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;//记得算空间、开ll 
const int MAXN=1e5;
bool vis[MAXN+5];
int a[MAXN+5],b[MAXN+5],data[MAXN+5],hou[MAXN+5],l[MAXN+5];
int sum[MAXN+5],id[MAXN+5],f[MAXN+5];
int n,m,k,head,tail;
int ans,Max;
multiset<int> t;
int main()
{
	//freopen("array.in","r",stdin);
	//freopen("array.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d%d",&a[i],&b[i]);
	//hou[a[i]]:第i个数a[i]第一次出现的位置 
	//l[i]:第i个数能向左边到的最远位置(没有与自己相同的) 
	for(int i=1;i<=n;i++)
		l[i]=hou[a[i]]+1,hou[a[i]]=i;
	for(int i=1;i<=n;i++)
		l[i]=max(l[i-1],l[i]);//左端点取最靠右的 
	head=1;
	for(int i=1;i<=n;i++)
	{
		k=i-1;
		//如果队首的位置小于l[i]那么head++
		while(head<tail&&id[head+1]<l[i])
		{
			t.erase(t.find(sum[head]));
			head++;
		}
		//如果队尾的b小于等于当前的b[i],那么tail--
		//(因为要像一个单调栈一样,有高的就把它踢掉)
		while(head<=tail&&b[i]>data[tail])
		{
			t.erase(t.find(sum[tail]));
			k=id[tail];
			tail--;
		}
		//处理完后现在i就可以进队了
		data[++tail]=b[i];
		id[tail]=k;
		/*若这个队列中不止一个数,
		那么除了要更改刚刚进来的数的值,
		现在还要更新一下最小的值(用一个set来维护),
		现在的head因为l[i]不一样了,
		所以初始的高度可能会不一样,所以需要重新入队一次*/
		if(head!=tail)
		{
			sum[tail]=f[id[tail]]+data[tail];
			t.insert(sum[tail]);
			t.erase(t.find(sum[head]));
		}
		sum[head]=f[l[i]-1]+data[head];
		t.insert(sum[head]);
		f[i]=*t.begin();
	}
	printf("%d",f[n]);
	return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值