SCOI2015 小凸玩密室

Link

Difficulty

算法难度5,思维难度7,代码难度6

Description

给定一棵 n n n个点的完全二叉树,树边带权,点带 v a l ( x ) val(x) val(x)的权值。

定义两个点的距离 d i s ( u , v ) dis(u,v) dis(u,v) u u u v v v的最短路径上经过的边权之和。

你一开始可以选择一个起始点,并点亮这个点,这一步不需要花费。

你在点亮一个点之后,必须紧接着将它的子树内所有点都点亮,才可以点亮其他点。

同时你需要满足点亮的点时刻在一个连通块内。

除了第一步,你点亮其他点所需的代价为 d i s ( u , v ) × v a l ( v ) dis(u,v)\times val(v) dis(u,v)×val(v),其中 u u u是你上一次点亮的点, v v v是你这次点亮的点。

1 ≤ n ≤ 2 × 1 0 5 , 1 ≤ v a l ≤ 1 0 5 1\le n\le 2\times 10^5,1\le val\le 10^5 1n2×105,1val105

Solution

我们考虑如何dp这个东西。

首先要考虑如果确定一个起点,答案会是什么样子的。

答案一定是:先每次选择一个儿子,填完这棵子树,再出来填另一棵;填完这个点为根的子树后,向父亲走,再去填另一棵子树,这样往复直到填完整棵树。

那么我们来考虑如何用设计dp状态,使得能用这个dp来凑出答案来。

假如我们设 d p ( x ) dp(x) dp(x)表示填完 x x x的子树所需的最小代价呢,显然这东西没法转移,因为你不知道你从哪儿来。

假如来个 d p ( i , j ) dp(i,j) dp(i,j)表示填完 i i i的子树且最后停在 j j j的最小代价呢,显然状态数太过庞大。

我们换个思路,既然没法记录从哪儿来,不如记录我们下一步去哪儿?

我们用 d p ( i , j ) dp(i,j) dp(i,j)表示填完 i i i的子树,且最后去点亮 j j j的最小代价,发现合法状态数非常少,是 O ( n l o g n ) O(nlogn) O(nlogn)级别的。

因为每个点填完子树之后,下一步只能点亮一个祖先或者祖先的另一个儿子。

具体来定义一下这个dp状态吧: d p ( i , j , 0 / 1 ) dp(i,j,0/1) dp(i,j,0/1)代表填完 i i i的子树, 0 0 0代表下一步填 j j j级祖先, 1 1 1代表下一步填 j + 1 j+1 j+1级祖先的另一个儿子,也就是 j j j级祖先的兄弟。

我们发现这个dp状态可以涵盖所有需要的信息,转移虽然有点儿复杂,但却十分自然。

来具体说一下状态转移吧:

  1. 如果这个点是叶子,也就是它没有儿子,那么只需要直接计算跳跃的代价就好了。
  2. 如果这个点有左儿子,那么只需要先走下去并计算这一步的代价,然后从左儿子跳到 j + 1 j+1 j+1级祖先即可。
  3. 如果这个点有左右儿子,那么就对于先走左边还是先走右边分类讨论,然后取min即可。

这样我们就可以处理出来所有dp值了。

考虑有了这些dp值,我们如何计算最终答案。

考虑枚举从哪个点开始,然后模拟填的过程:先每次选择一个儿子,填完这棵子树,再出来填另一棵;填完这个点为根的子树后,向父亲走,再去填另一棵子树,这样往复直到填完整棵树。

然而这之中会有点儿小问题,就是走到根之后很难继续走下去了,我们发现没有合适的状态供我们使用了。

我们可以添加一个虚拟节点 0 0 0号点,它的点权为 0 0 0,它连一条向 1 1 1的边,权值为 0 0 0

这样一切问题都可以完美地解决了,因为我们的策略总是最后走到 0 0 0号点然后停下,然而 0 0 0号点权值为 0 0 0,对答案毫无影响。

这样我们就可以用 O ( n l o g n ) O(nlogn) O(nlogn)的时间复杂度来解决这个题了。

我思考了一下,觉得dp的一大关键就是如何用状态拼出来结果,自己体会一下吧。

写这个题的时候需要注意边界问题。

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
#define LL long long
using namespace std;
inline int read(){
	int x=0,f=1;char ch=' ';
	while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0' && ch<='9')x=x*10+(ch^48),ch=getchar();
	return f==1?x:-x;
}
const int N=2e5+5;
int n;
LL a[N],val[N];
LL st[N][20],dis[N][20],dp[N][20][2];
inline void solve(){
	for(int i=1;i<=n;++i)st[i][0]=i,dis[i][0]=0;
    for(int i=1;i<=19;++i)st[0][i]=-1;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=19;++j){
			if((1<<(j-1))<=i)st[i][j]=i>>j;
			else st[i][j]=-1;
			if(~st[i][j])dis[i][j]=dis[st[i][1]][j-1]+val[i];
		}
	for(int i=n;i>=1;--i){
		for(int j=0;~st[i][j] && j<=18;++j){
			if((i<<1)>n){
				if(~st[i][j])dp[i][j][0]=dis[i][j]*a[st[i][j]];
				if(~st[i][j+1])dp[i][j][1]=(dis[i][j+1]+val[st[i][j]^1])*a[st[i][j]^1];
			}
			else if((i<<1|1)>n){
				if(~st[i][j])dp[i][j][0]=dp[i<<1][j+1][0]+val[i<<1]*a[i<<1];
				if(~st[i][j+1])dp[i][j][1]=dp[i<<1][j+1][1]+val[i<<1]*a[i<<1];
			}
			else{
				if(~st[i][j])dp[i][j][0]=min(val[i<<1]*a[i<<1]+dp[i<<1][0][1]+dp[i<<1|1][j+1][0],val[i<<1|1]*a[i<<1|1]+dp[i<<1|1][0][1]+dp[i<<1][j+1][0]);
				if(~st[i][j+1])dp[i][j][1]=min(val[i<<1]*a[i<<1]+dp[i<<1][0][1]+dp[i<<1|1][j+1][1],val[i<<1|1]*a[i<<1|1]+dp[i<<1|1][0][1]+dp[i<<1][j+1][1]);
			}
		}
	}
	LL ans=0x3f3f3f3f3f3f3f3f;
	for(int i=1;i<=n;++i){
		LL tmp=dp[i][1][0];
		for(int j=st[i][1],last=i;j;last=j,j=st[j][1]){
			if(last&1)
				tmp+=val[last-1]*a[last-1]+dp[last-1][2][0];
			else if((last+1)<=n)
				tmp+=val[last+1]*a[last+1]+dp[last+1][2][0];
			else
				tmp+=val[j]*a[st[j][1]];
		}
		ans=min(ans,tmp);
	}
	printf("%lld\n",ans);
}
int main(){
	memset(dp,0x3f,sizeof dp);
	n=read();
	for(int i=1;i<=n;++i)a[i]=read();
	for(int i=2;i<=n;++i)val[i]=read();
	solve();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值