洛谷P3571 [POI2014]SUP-Supercomputer 斜率优化dp

题目链接:传送门

题目大意:给一棵 n n n个节点、以 1 1 1为根的树。有 Q Q Q次询问,每次给出一个 K K K,用最少的操作次数遍历完整棵树,输出最少操作次数。每次操作可以选择访问不超过 K K K个未访问的点,且这些点的父亲必须在之前被访问过。

需要点亮的技能:

1. d p 1.dp 1.dp(雾)
2. 2. 2.斜率(大雾)
3. 3. 3.斜率优化基本操作

首先题目的样例长相奇特,在这里解释一下:
(借用一下这个题解的图,虽然他的样例解释是错的qwq

样例的 K = 3 K=3 K=3,即每次最多珂以访问3个父节点已被访问过的节点。
最少要8次操作遍历完整棵树:

1次:12次:2,3,43次:5,6,84次:7,9,105次:11,126次:17,18,197次:13,14,208次:15,16

Step 1.贪心

思考如何让操作次数最小:
每次尽量多访问节点,且尽量访问有儿子的节点。 {\color{red}\text{每次尽量多访问节点,且尽量访问有儿子的节点。}} 每次尽量多访问节点,且尽量访问有儿子的节点。
尽量访问有儿子的节点与都访问没有儿子的节点相比,下一次可访问的节点数量会更多,因此尽量访问有儿子的节点会更优。
这样贪心之后答案一定正确。
然后发现,每次贪心复杂度是 O ( n ) O(n) O(n),总共贪心 q q q次,因此总时间复杂度是 O ( n q ) O(nq) O(nq),显然过不了qwq( O ( n q ) O(nq) O(nq)过百万?)。

Step 2.dp

如果珂以把答案预处理出来,就珂以 O ( 1 ) O(1) O(1)查询了qwq。
这里yy一下, 显然这是个 d p dp dp
d p [ i ] dp[i] dp[i]表示当 K = i K=i K=i,即每次操作最多访问 i i i个节点时,所需要的最少操作次数。
然后发现这个东西好像递推不了QWQ?
某不知名dalao:于是我们可以考虑上网搜题解。

考虑从 j j j推出 i i i
s [ i ] s[i] s[i]表示深度 > i >i >i的节点个数。
d p [ i ] = m a x ( j + ⌈ s [ j ] i ⌉ ) dp[i]=max(j+\lceil \frac{s[j]}{i} \rceil) dp[i]=max(j+is[j]) (毒瘤!) {\color{red}\text{(毒瘤!)}} (毒瘤!)
思考几个问题: {\color{red}\text{思考几个问题:}} 思考几个问题:

1.如何解释这个转移方程?(为什么其他题解都说这个式子显然呢qwq)
2.为什么是max而不是min?

方程表示的是用 j j j次访问完前 j j j层节点,之后每次操作,除最后一次访问完所有节点外,都可以访问 i i i次节点。
那么这为什么是正确的呢? {\color{red}\text{那么这为什么是正确的呢?}} 那么这为什么是正确的呢?

脑补一下发现,大部分情况下, j + ⌈ s [ j ] i ⌉ j+\lceil \frac{s[j]}{i} \rceil j+is[j]是比 K = i K=i K=i时的真实最小操作数小的!

因此这里要取 m a x max max值。
但怎么证明这个 m a x max max值就一定为 d p [ i ] dp[i] dp[i]呢?
由贪心策略可知,取完前 j j j层之后,每次应该尽量访问有儿子的节点。

如果此时能访问的节点数不超过 i i i,则可以把第 j + 1 j+1 j+1层节点取完,于是用 j + 1 j+1 j+1次取完前 j + 1 j+1 j+1层时的最小操作次数也会计入答案,且这个答案会比当前第 j j j层的答案大(比较显然)。

如果此时能访问的节点超过 i i i,那么按照贪心策略优先访问有儿子的节点。
再分类:
1. 1. 1.如果有儿子的节点的个数不超过 i i i,则把有儿子的节点访问完,和无儿子的节点共访问其中 i i i个。
2. 2. 2.如果有儿子的节点的个数超过 i i i,则不断访问 i i i个有儿子的节点,直到有儿子的节点的个数不超过 i i i,然后回到1。
yy一下, 显然这样最后每次操作可以访问 i i i个节点。
于是 d p dp dp方程就解释完了。
(为什么其他题解几句话就完事……估计是我太蒻了qwq)

Step 3.斜率优化

按照套路,若 j > k j>k j>k j j j k k k优,则:
j + ⌈ s [ j ] i ⌉ > k + ⌈ s [ k ] i ⌉ j+\lceil \frac{s[j]}{i} \rceil>k+\lceil \frac{s[k]}{i} \rceil j+is[j]>k+is[k]
通分,得 ⌈ i ∗ j + s [ j ] i ⌉ > ⌈ i ∗ k + s [ k ] i ⌉ \lceil \frac{i*j+s[j]}{i} \rceil>\lceil \frac{i*k+s[k]}{i} \rceil iij+s[j]>iik+s[k]
两边同乘 i i i,把取整消掉,得 i ∗ j + s [ j ] > i ∗ k + s [ k ] i*j+s[j]>i*k+s[k] ij+s[j]>ik+s[k]
乱移个项,得 s [ j ] − s [ k ] > i ∗ ( k − j ) s[j]-s[k]>i*(k-j) s[j]s[k]>i(kj)
因为 j &gt; k j&gt;k j>k,即 j − k &gt; 0 , k − j &lt; 0 j-k&gt;0 , k-j&lt;0 jk>0,kj<0
所以 s [ j ] − s [ k ] j − k &gt; i \frac{s[j]-s[k]}{j-k}&gt;i jks[j]s[k]>i (乘一些奇奇怪怪的 − 1 -1 1之类的)。
然后就是斜率优化基本套路辣QuQ~
不过这题中 d p [ i ] dp[i] dp[i]是从 [ 1 , m a x d e e p ] [1,maxdeep] [1,maxdeep]推来的,所以一开始要把所有可能的 j j j入队,然后转移时只用考虑出队。

代码

丑陋、毒瘤的代码:
(忘记输出空格调了一年QAQ)

#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
#define rl register ll
#define lc rt<<1
#define rc rt<<1|1
using namespace std;
typedef long long ll;
int read() {
	re x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9') {
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9') {
		x=10*x+ch-'0';
		ch=getchar();
	}
	return x*f;
}
const int Size=1000005;
ll n,q,dp[Size],s[Size],k[Size];
int hd,tl,Queue[Size];
inline double slope(int i,int j) {
	return (s[j]-s[i])/(j-i);
}
int cnt,maxd,head[Size],deep[Size],num[Size];
struct Edge {
	int v,next;
} w[Size<<1];
void AddEdge(int u,int v) {
	w[++cnt].v=v;
	w[cnt].next=head[u];
	head[u]=cnt;
}
void dfs(int x,int fa) {
	deep[x]=deep[fa]+1;
	if(deep[x]>maxd) {
		maxd=deep[x];
	}
	num[deep[x]]++;
	for(int i=head[x]; i; i=w[i].next) {
		dfs(w[i].v,x);
	}
}
int main() {
	n=read();
	q=read();
	int maxk=0;
	for(re i=1; i<=q; i++) {
		k[i]=read();
		if(k[i]>maxk)	maxk=k[i];
	}
	for(re i=2; i<=n; i++) {
		int f=read();
		AddEdge(f,i);
	}
	dfs(1,0);
	for(re i=maxd; i; i--) {
		s[i]=s[i+1]+num[i+1];
	}
	hd=tl=0;
	for(re i=1; i<=maxd; i++) {
		while(hd<tl && slope(Queue[tl],i)>=slope(Queue[tl-1],Queue[tl]))	tl--;
		Queue[++tl]=i;
	}
	for(rl i=1; i<=maxk; i++) {
		while(hd<tl && i*Queue[hd+1]+s[Queue[hd+1]]>=i*Queue[hd]+s[Queue[hd]])	hd++;
		dp[i]=Queue[hd]+ceil(1.0*s[Queue[hd]]/i);
	}
	for(re i=1; i<=q; i++) {
		printf("%lld ",dp[k[i]]);
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值