有先后限制的一类贪心问题

39 篇文章 0 订阅
21 篇文章 0 订阅

LOJ#2509. 「AHOI / HNOI2018」排列
这个题实质上是一棵树上满足父亲比儿子先选,第i个选的点 x x x的有 i ∗ w [ x ] i*w[x] iw[x]的贡献,求最大贡献。
可以发现 w [ x ] w[x] w[x]最小的应该尽可能先的被选。
于是我们考虑将最小的点 x x x f a x fa_x fax缩为一个点。
然后我们考虑到缩为一个点之后的新 w w w是一个问题。
考虑两个没有限制的树的先后选关系,缩为两个点之后分别为 ( a , b ) , ( c , d ) (a,b),(c,d) (a,b),(c,d)
其中 ( x , y ) (x,y) (x,y)表示大小为 x x x w w w和为 y y y

首先我们可以举一些小例子,比如一个树是 w [ x ] = 1 − > w [ y ] = 2 w[x] = 1-> w[y] = 2 w[x]=1>w[y]=2,发现另一个树是 w [ z ] = 1.5 w[z] = 1.5 w[z]=1.5时先后选没有区别。
那么就猜测先后选与平均值有关系。

去dalao博客看证明吧
经过yy可以发现这是对的。
那么对于缩完点后的 ( x , y ) (x,y) (x,y) y x \frac yx xy为关键字每次取最小的和父亲合并。
使用set维护。

AC Code:

#include<bits/stdc++.h>
#define maxn 500005
#define LL long long
using namespace std;

int n,w[maxn],a[maxn];
int info[maxn],Prev[maxn],to[maxn],cnt_e;
void Node(int u,int v){ Prev[++cnt_e]=info[u],info[u]=cnt_e,to[cnt_e]=v; }

struct node{
	int l,id;LL s,a;
	node(int l=0,int id=0,LL s=0,LL a=0):l(l),id(id),s(s),a(a){}
	bool operator <(const node &B)const{ return s*B.l == l*B.s ? id < B.id : s*B.l < l*B.s; }
	node operator +(const node &B)const{ return node(l+B.l,id,s+B.s,a+B.a+B.s*l); }
}ar[maxn];

int F[maxn];
int Find(int now){ return F[now] == -1 ? now : F[now] = Find(F[now]); }

int main(){
	scanf("%d",&n);
	int rt = 0;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]) , (!a[i]) && (rt = i);
	multiset<node>st;
	for(int i=1;i<=n;i++) scanf("%d",&w[i]),st.insert(ar[i] = node(1,i,w[i],w[i]));
	st.insert(ar[0] = node(0,0,0,0));
	if(!rt){ puts("-1");return 0; }//它居然过了这水数据
	memset(F,-1,sizeof F);
	for(;!st.empty();){
		int lc = (*st.begin()).id , y = Find(a[lc]);
		st.erase(st.begin());
		if(!lc) continue;
		if(y) st.erase(st.find(ar[y]));
		ar[y] = ar[y] + ar[lc];
		F[lc] = y;
		if(y) st.insert(ar[y]);
	}
	printf("%lld\n",ar[0].a);
}

合并果子 ⋅ \cdot
小象有 n n n堆果子排成一列,每堆果子有个权值。小象一开始可以选择一堆果子。接下来每一轮,小象可以选择将这堆果子与左边或者右边的果子合并,形成一堆新的果子,在新的果子上继续进行上面的操作。进行 n − 1 n-1 n1轮后,合并成一堆果子。每次合并两堆果子的代价为两堆果子的重量之和。
小象想知道,对于每堆果子,如果小象选择以这堆果子作为初始选择,那么合并的最小代价是什么。
n &lt; = 2 e 5 n&lt;=2e5 n<=2e5

这个发现也是一棵树,求和的式子推一下也一样。
不过是根有两个儿子,其余点只有一个儿子的那种树。
但是我们需要对于链上每个点为根都求答案,之前的方法就不管用了。

这个题有一个很巧也很短的 O ( n 2 ) O(n^2) O(n2)解法:

#include<bits/stdc++.h>
#define LL long long
using namespace std;

int n,a[5005];
LL sum[5005],dp[5005][5005];

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]),sum[i]=sum[i-1] + a[i];
	memset(dp,0x3f,sizeof dp);
	dp[1][n] = 0;
	for(int l=n-1;l>=1;l--)
		for(int st=1;st+l<=n;st++)
			dp[st+1][st+l] = min(dp[st+1][st+l] , dp[st][st+l] + sum[st+l] - sum[st-1]),
			dp[st][st+l-1] = min(dp[st][st+l-1] , dp[st][st+l] + sum[st+l] - sum[st-1]);
	for(int i=1;i<=n;i++)
		printf("%lld\n",dp[i][i]);
}

但是毒瘤的dls把它出到了 2 e 5 2e5 2e5
考虑每个点向上合并的过程。
那么对于根左边链上的点,是多条连续的子链分批次的合并到根上,这些子链 ( x , y ) (x,y) (x,y) y x \frac yx xy按从小到大的顺序合并到根上。左右两边链构成了一个归并关系。
这个归并其实并没有那么好的性质,我们直接用平衡树可以求最大值,同时支持插入删除 ( x , y ) (x,y) (x,y)
然后我们可以单调栈求出每一个 ( x , y ) (x,y) (x,y)根在哪个位置时开始出现,根在哪个位置时结束出现(可以发现每个 ( x , y ) (x,y) (x,y)出现的位置是连续的)
然后再扫一遍维护平衡树来插删 ( x , y ) (x,y) (x,y)求每个点为根的答案。

毒瘤。
AC Code:

#include<bits/stdc++.h>
#define maxn 500005
#define LL long long
using namespace std;

int n,w[maxn],a[maxn];
int info[maxn],Prev[maxn],to[maxn],cnt_e;
void Node(int u,int v){ Prev[++cnt_e]=info[u],info[u]=cnt_e,to[cnt_e]=v; }

struct node{
	int l,id;LL s,a;
	node(int l=0,int id=0,LL s=0,LL a=0):l(l),id(id),s(s),a(a){}
	bool operator <(const node &B)const{ return s*B.l == l*B.s ? id < B.id : s*B.l < l*B.s; }
	node operator +(const node &B)const{ return node(l+B.l,id,s+B.s,a+B.a+B.s*l); }
}ar[maxn];

int F[maxn];
int Find(int now){ return F[now] == -1 ? now : F[now] = Find(F[now]); }

int main(){
	scanf("%d",&n);
	int rt = 0;
	for(int i=1;i<=n;i++) scanf("%d",&a[i]) , (!a[i]) && (rt = i);
	multiset<node>st;
	for(int i=1;i<=n;i++) scanf("%d",&w[i]),st.insert(ar[i] = node(1,i,w[i],w[i]));
	st.insert(ar[0] = node(0,0,0,0));
	if(!rt){ puts("-1");return 0; }
	memset(F,-1,sizeof F);
	for(;!st.empty();){
		int lc = (*st.begin()).id , y = Find(a[lc]);
		st.erase(st.begin());
		if(!lc) continue;
		if(y) st.erase(st.find(ar[y]));
		ar[y] = ar[y] + ar[lc];
		F[lc] = y;
		if(y) st.insert(ar[y]);
	}
	printf("%lld\n",ar[0].a);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值