test20190909 Gluttony

0+0+0+0+0+0=0。毒瘤出题人。

BJOI2019 勘破神机

地灾军团的军师黑袍从潜伏在精灵高层的密探手中得知了神杖的情报,他对奥术宝石中蕴含的远古神秘力量十分感兴趣。他设计夺取了数块奥术宝石,并命令作为地灾军团首席科学家的你带领手下的研究人员全力破解。经过了一个月的艰苦尝试,你的研究团队终于破译了 “2” 型奥术宝石和 “3” 型奥术宝石的内部能量结构。

这两类结构有着一定的相似性,它们的内部具有 \(k\) 个反应核心,“2” 型奥术宝石的每个核心都可以看成是一个 \(2 \times n\) 的网格,而 “3” 型奥术宝石的每个核心都可以看成是一个 \(3 \times n\) 的网格。(注意奥术宝石的 \(k\)\(n\) 可能不同)当神力反应进行时,每个核心自动填充满神力颗粒。

形式化地描述,每个神力颗粒可以看成是一个 \(1 \times 2\) 横置或竖置的方格,核心填满的定义为每个网格都恰好被一某个方格覆盖。若在两种填满反应核心的方案中存在一个方格放置的位置或方式不同,就认为方案不同。

如填满 \(2×4\) 的网格有 \(5\) 种不同的方案,填满 \(3×2\) 的网格有 \(3\) 种不同的方案。

勘破神机

如果奥术宝石的 \(k\) 个核心的填充方式互不相同,它们就会组合出强大的咒术。黑袍想知道对于某个宝石一共有多少种不同的咒术(对于两种咒术组合,如果第一种咒术中每个核心 \(a\) 的填充方式都可以找到第二种咒术的某个核心 \(b\),使得 \(a\)\(b\) 的填充方式完全相同,则认为这两种咒术组合相同)。

对于宽度为 \(n\) 、反应核心个数为 $k $的 “2” 型奥术宝石,设不同的咒术为 \(F(n,k) ;\)对于宽度为 \(n\) 、反应核心个数为 \(k\) 的 “3” 型奥术宝石,设不同的咒术为 \(G(n,k)\) 。例如 \(F(4,1) = 5\)\(F(4,2) = 10\)\(G(2,2) = 3\)

地灾军团的科技水平还不能精准测量反应核心的长度 \(n\) ,只能确定出核心长度的大致范围 \([l,r]\) 。你需要计算出反应核心长度在此区间内的平均咒术数,即

\[ans_2 = \frac{1}{r-l+1}\sum_{n=l}^{r} F(n,k)\\ ans_3 = \frac{1}{r-l+1}\sum_{n=l}^{r} G(n,k) \]

设最终答案的形式为 \(\frac{A}{B}\),输出 \(A \times B^{-1} \bmod 998244353\) 的结果,其中 \(B^{-1}\)\(B\) 在模 \(998244353\) 下的乘法逆元。

\(1 \leq l \leq r \leq 10^{18},1\leq k\leq 501\),对于所有数据,保证不存在 \(r-l+1\)\(998244353\) 倍数的情况。

题解

首先推 \(F(n)\)\(G(n)\) 的式子。\(G\) 只对偶数讨论。

我们枚举最后一个整块的划分,可以发现

\[F(n)=F(n-1)+F(n-2)\\ G(n)=3G(n-1)+2\sum_{i=0}^{n-2}G(i) \]

\(G\) 做个差分,得到

\[G(n)=4G(n-1)-G(n-2) \]

用特征方程求出通项公式

\[F(n)=\frac{5+\sqrt 5}{10}\left(\frac{1+\sqrt 5}{2}\right)^n+\frac{5-\sqrt 5}{10}\left(\frac{1-\sqrt 5}{2}\right)^n\\ G(n)=\frac{3+\sqrt 3}{6}(2+\sqrt 3)^n+\frac{3-\sqrt 3}{6}(2-\sqrt 3)^n \]

显然答案要求

\[ans_2=\frac{1}{r-l+1}\sum_{n=l}^r\binom{F(n)}{k}\\ ans_3=\frac{1}{r-l+1}\sum_{n=l}^r\binom{G(n)}{k} \]

这两个式子形式一样,我们对 \(ans_2\) 分析

\[(r-l+1)ans_2=\frac{1}{k!}\sum_{n=l}^rF(n)^{\underline k} \]

由通项公式的二项加法形式和下降幂,我们可以联想到用斯特林数将下降幂转化成普通幂,然后对二项加法形式的普通幂进行二项式展开。

所以要求的就是

\[\sum_{n=l}^rF(n)^\underline k=\sum_{n=l}^r\sum_{i=0}^k(-1)^{k-i}\begin{bmatrix}k \\ i\end{bmatrix} F(n)^i\\ \]

\(F(n)\) 看成 \(A\alpha^n+B\beta^n\),则

\[\sum_{n=l}^rF(n)^\underline k=\sum_{n=l}^r\sum_{i=0}^k(-1)^{k-i}\begin{bmatrix}k \\ i\end{bmatrix}\sum_{j=0}^i\binom{i}{j}A^j\alpha^{jn}B^{i-j}\beta^{(i-j)n} \]

把跟 \(n\) 无关的提到前面去

\[\sum_{n=l}^rF(n)^\underline k=\sum_{i=0}^k(-1)^{k-i}\begin{bmatrix}k \\ i\end{bmatrix}\sum_{j=0}^i\binom{i}{j}A^jB^{i-j}\sum_{n=l}^r(\alpha^j\beta^{i-j})^n \]

由于 \(k\) 很小,所以前面两个求和直接枚举,最后一个就用等比数列求和公式就好了。

二次剩余不存在,需要自己定义复数域。

黄毒瘤:搬原题没有素质,我再加个 Pollard-Rho。

#include<bits/stdc++.h>
using namespace std;
template<class T> T read(){
    T x=0,w=1;char c=getchar();
    for(;!isdigit(c);c=getchar())if(c=='-') w=-w;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x*w;
}
template<class T> T read(T&x){
    return x=read<T>();
}
#define co const
#define il inline
typedef long long LL;

co int mod=998244353,i2=499122177,i10=299473306,i6=166374059;
il int add(int a,int b){
	return (a+=b)>=mod?a-mod:a;
}
il int mul(int a,int b){
	return (LL)a*b%mod;
}
il int fpow(int a,int b){
	int ans=1;
	for(;b;b>>=1,a=mul(a,a))
		if(b&1) ans=mul(ans,a);
	return ans;
}

int omega;
struct $ {int x,y;};
il $ operator+(co $&a,int b){
	return ($){add(a.x,b),a.y};
}
il $ operator+(co $&a,co $&b){
	return ($){add(a.x,b.x),add(a.y,b.y)};
}
il $ operator-(co $&a,co $&b){
	return ($){add(a.x,mod-b.x),add(a.y,mod-b.y)};
}
il $ operator*(co $&a,int b){
	return ($){mul(a.x,b),mul(a.y,b)};
}
il $ operator*(co $&a,co $&b){
	return ($){add(mul(a.x,b.x),mul(omega,mul(a.y,b.y))),add(mul(a.x,b.y),mul(a.y,b.x))};
}
il $ pow($ a,LL b){
	$ ans=($){1,0};
	for(;b;b>>=1,a=a*a)
		if(b&1) ans=ans*a;
	return ans;
}
il $ inv(co $&a){
	$ b=($){a.x,mod-a.y};
	return b*fpow((a*b).x,mod-2);
}

int S[502][502],C[502][502];

int main(){
//	cerr<<2<<" "<<fpow(2,mod-2)<<endl;
//	cerr<<10<<" "<<fpow(10,mod-2)<<endl;
//	cerr<<6<<" "<<fpow(6,mod-2)<<endl;
	read<int>(); // T=1
	int m=read<int>();
	LL L=read<LL>(),R=read<LL>(),K=read<LL>();
	
	S[0][0]=1;
	for(int i=1;i<=K;++i)
		for(int j=1;j<=i;++j) S[i][j]=add(S[i-1][j-1],mod-mul(i-1,S[i-1][j]));
	C[0][0]=1;
	for(int i=1;i<=K;++i){
		C[i][0]=C[i][i]=1;
		for(int j=1;j<i;++j) C[i][j]=add(C[i-1][j],C[i-1][j-1]);
	}
	
	int len=(R-L+1)%mod;
	$ A,B,alpha,beta;
	if(m==2){
		omega=5;
		A=($){i2,i10},B=($){i2,mod-i10};
		alpha=($){i2,i2},beta=($){i2,mod-i2};
	}
	else{
		omega=3;
		L=(L+1)>>1,R=R>>1;
		A=($){i2,i6},B=($){i2,mod-i6};
		alpha=($){2,1},beta=($){2,mod-1};
	}
	
	int ans=0,nlen=(R-L+1)%mod;
	for(int i=0;i<=K;++i){
		$ sum=($){0,0};
		for(int j=0;j<=i;++j){
			$ t=pow(A,j)*pow(B,i-j)*C[i][j];
			$ ab=pow(alpha,j)*pow(beta,i-j);
			if(ab.x==1 and ab.y==0) sum=sum+t*nlen; // edit 1
			else{
				$ sl=(pow(ab,L)+(mod-1))*inv(ab+(mod-1));
				$ sr=(pow(ab,R+1)+(mod-1))*inv(ab+(mod-1));
				sum=sum+t*(sr-sl);
			}
		}
		ans=add(ans,mul(S[K][i],sum.x));
	}
		
	ans=mul(ans,fpow(len,mod-2));
	for(int i=1;i<=K;++i) ans=mul(ans,fpow(i,mod-2));
	printf("%d\n",ans);
	return 0;
}

注意公比为 \(1\) 的情况。

NOI2009 植物大战僵尸

Plants vs. Zombies (PVZ) 是最近十分风靡的一款小游戏。Plants(植物)和 Zombies(僵尸)是游戏的主角,其中Plants防守,而Zombies进攻。该款游戏包含多种不同的挑战系列,比如Protect Your BrainBowling等等。其中最为经典的,莫过于玩家通过控制Plants来防守Zombies的进攻,或者相反地由玩家通过控制ZombiesPlants发起进攻。

现在,我们将要考虑的问题是游戏中ZombiesPlants的进攻,请注意,本题中规则与实际游戏有所不同。游戏中有两种角色,PlantsZombies,每个Plant有一个攻击位置集合,它可以对这些位置进行保护;而Zombie进攻植物的方式是走到植物所在的位置上并将其吃掉。

游戏的地图可以抽象为一个 N 行 M 列的矩阵,行从上到下用 0 到 N–1 编号,列从左到右用 0 到 M–1 编号;在地图的每个位置上都放有一个Plant,为简单起见,我们把位于第 r 行第 c 列的植物记为 Pr,c

Plants分很多种,有攻击类、防守类和经济类等等。为了简单的描述每个Plant,定义 Score 和 Attack 如下:

  • Score[Pr,c]
    Zombie击溃植物 Pr,c 可获得的能源。若 Score[Pr,c] 为非负整数,则表示击溃植物 Pr,c 可获得能源 Score[Pr,c],若为负数表示击溃 Pr,c 需要付出能源 -Score[Pr,c]。
  • Attack[Pr,c]
    植物 Pr,c 能够对Zombie进行攻击的位置集合。

Zombies必须从地图的右侧进入,且只能沿着水平方向进行移动。Zombies攻击植物的唯一方式就是走到该植物所在的位置并将植物吃掉。因此Zombies的进攻总是从地图的右侧开始。也就是说,对于第 r 行的进攻,Zombies必须首先攻击 Pr,M-1;若需要对 Pr,c (0≤c<M-1) 攻击,必须将 Pr,M-1, Pr, M-2 … Pr, c+1 先击溃,并移动到位置 (r, c) 才可进行攻击。

在本题的设定中,Plants的攻击力是无穷大的,一旦Zombie进入某个Plant的攻击位置,该Zombie会被瞬间消灭,而该Zombie没有时间进行任何攻击操作。因此,即便Zombie进入了一个Plant所在的位置,但该位置属于其他植物的攻击位置集合,则Zombie会被瞬间消灭而所在位置的植物则安然无恙(在我们的设定中,Plant的攻击位置不包含自身所在位置,否则你就不可能击溃它了)。

Zombies的目标是对Plants的阵地发起进攻并获得最大的能源收入。每一次,你可以选择一个可进攻的植物进行攻击。本题的目标为,制定一套Zombies的进攻方案,选择进攻哪些植物以及进攻的顺序,从而获得最大的能源收入。

100%的数据满足 1 ≤ N ≤ 20,1 ≤ M ≤ 30,-10000 ≤ Score ≤ 10000

题解

首先用拓扑排序判掉有环的无解情况,进行 DFS 缩减点数。

然后问题装化成了最大权闭合子图问题,最小割建模解决。

建图

  1. 樟脑丸 i 的点权设为 wi
  2. 所有樟脑丸向它的右边的樟脑丸连边
  3. 如果樟脑丸 i 的位置是樟脑丸 j 的有效位置,i 向 j 连边

在这个图上求出最大权闭合子图即可。

黄毒瘤:蟑螂大战樟脑丸

#include<bits/stdc++.h>
using namespace std;
template<class T> T read(){
    T x=0,w=1;char c=getchar();
    for(;!isdigit(c);c=getchar())if(c=='-') w=-w;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x*w;
}
template<class T> T read(T&x){
    return x=read<T>();
}
#define co const
#define il inline
typedef long long LL;

co int N=610,INF=1e9;
namespace Flow{
	int n,s,t;
	vector<int> to[N],nx[N],cap[N];
	int dis[N];
	deque<int> q;
	
	il void add_edge(int u,int v,int c){
		to[u].push_back(v),cap[u].push_back(c);
		to[v].push_back(u),cap[v].push_back(0);
		nx[u].push_back(to[v].size()-1),nx[v].push_back(to[u].size()-1);
	}
	bool bfs(){
		fill(dis+1,dis+n+1,INF),dis[t]=0;
		q.push_back(t);
		while(q.size()){
			int x=q.front();q.pop_front();
			for(int i=0;i<(int)to[x].size();++i){
				int y=to[x][i],c=cap[y][nx[x][i]];
				if(c and dis[y]==INF) dis[y]=dis[x]+1,q.push_back(y);
			}
		}
		return dis[s]<INF;
	}
	int dfs(int x,int lim){
		if(x==t) return lim;
		int re=lim;
		for(int i=0;re and i<(int)to[x].size();++i){
			int y=to[x][i],&c=cap[x][i];
			if(c and dis[y]==dis[x]-1){
				int delta=dfs(y,min(re,c));
				if(!delta) {dis[y]=INF;continue;}
				re-=delta,c-=delta,cap[y][nx[x][i]]+=delta;
			}
		}
		return lim-re;
	}
	int main(){
		int ans=0;
		while(bfs()) ans+=dfs(s,INF);
		return ans;
	}
}

int n,m,val[N];
vector<int> to[N];
int deg[N],vis[N];

il int idx(int x,int y){
	return (x-1)*m+y;
}
int main(){
	read(n),read(m);
	Flow::n=n*m+2,Flow::s=Flow::n-1,Flow::t=Flow::n;
	for(int i=1;i<=n*m;++i){
		read(val[i]);
		if(i%m) to[i+1].push_back(i),++deg[i];
		int t=read<int>();
		for(int j=1;j<=t;++j){
			int nx=read<int>()+1,ny=read<int>()+1;
			to[i].push_back(idx(nx,ny)),++deg[idx(nx,ny)];
		}
	}
	// topological sort
	deque<int> q;
	for(int i=1;i<=n*m;++i)
		if(!deg[i]) q.push_back(i);
	while(q.size()){
		int x=q.front();
		vis[x]=1,q.pop_front();
		for(int i=0;i<(int)to[x].size();++i){
			int y=to[x][i];
			if(--deg[y]==0) q.push_back(y);
		}
	}
	// maximum weight closed subgraph
	int ans=0;
	for(int i=1;i<=n*m;++i)if(vis[i]){
		if(val[i]>=0) ans+=val[i],Flow::add_edge(Flow::s,i,val[i]);
		else Flow::add_edge(i,Flow::t,-val[i]);
		for(int j=0;j<(int)to[i].size();++j)
			if(vis[to[i][j]]) Flow::add_edge(to[i][j],i,INF);
	}
	printf("%d\n",ans-Flow::main());
	return 0;
}

BZOJ2144 跳跳棋

跳跳棋是在一条数轴上进行的。棋子只能摆在整点上。每个点不能摆超过一个棋子。我们用跳跳棋来做一个简单的游戏:棋盘上有3颗棋子,分别在a,b,c这三个位置。我们要通过最少的跳动把他们的位置移动成x,y,z。(棋子是没有区别的)跳动的规则很简单,任意选一颗棋子,对一颗中轴棋子跳动。跳动后两颗棋子距离不变。一次只允许跳过1颗棋子。

跳跳棋

写一个程序,首先判断是否可以完成任务。如果可以,输出最少需要的跳动次数。

100%的数据绝对值不超过109

题解

只有三种跳跃方式:

  1. 中间的向左跳。
  2. 中间的向右跳。
  3. 左右中距离中间最近的点向中间跳。

考虑三个点的相对距离的变化。中间的点往左右跳距离增大,左右的点往中间跳距离减小,直到距离相等为止。考虑左右两个点往中间跳时相对距离的变化,发现这其实就是一个辗转相减的过程。所以这是一个无限的二叉树结构。

判断是否可行就等价于判断初始状态和目标状态是否在同一颗树中,只需要找出树根即可。

求最少步数就等价于求初始状态和目标状态在树上的距离。求距离的话,先将初始状态和目标状态跳到同一深度,再二分它们到 LCA 的距离即可。

黄毒瘤:显然

#include<bits/stdc++.h>
using namespace std;
template<class T> T read(){
    T x=0,w=1;char c=getchar();
    for(;!isdigit(c);c=getchar())if(c=='-') w=-w;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x*w;
}
template<class T> T read(T&x){
    return x=read<T>();
}
#define co const
#define il inline
typedef long long LL;

co int INF=1e9;
struct node{
	int x,y,z;
	il bool operator==(co node&t)co{
		return x==t.x and y==t.y and z==t.z;
	}
	il bool operator!=(co node&t)co{
		return x!=t.x or y!=t.y or z!=t.z;
	}
	void input(){
		static int tmp[3];
		for(int i=0;i<3;++i) read(tmp[i]);
		sort(tmp,tmp+3);
		x=tmp[0],y=tmp[1],z=tmp[2];
	}
}a,b;

node get_fa(node now,int lim,int&step){
	int k;
	for(step=0;lim;step+=k){
		int x=now.y-now.x,y=now.z-now.y;;
		if(x==y) break;
		if(x<y){
			k=min((y-1)/x,lim);
			now.x+=k*x,now.y+=k*x;
			lim-=k;
		}
		else{
			k=min((x-1)/y,lim);
			now.y-=k*y,now.z-=k*y;
			lim-=k;
		}
	}
	return now;
}

int main(){
	a.input(),b.input();
	int da,db;
	if(get_fa(a,INF,da)!=get_fa(b,INF,db)){
		puts("NO");
		return 0;
	}
	if(da<db) swap(da,db),swap(a,b);
	
	int tmp;
	a=get_fa(a,da-db,tmp);
	
	int l=0,r=INF;
	while(l<r){
		int mid=(l+r)>>1;
		if(get_fa(a,mid,tmp)==get_fa(b,mid,tmp)) r=mid;
		else l=mid+1;
	}
	printf("YES\n%d\n",(l<<1)+(da-db));
	return 0;
}

附赠以前一道也是在gcd上面搞事情的题。

test20181029 数列

考场做法

打表发现,最后的循环节一定是\(\gcd(a_1,a_2),\gcd(a_1,a_2),0\)这种形式,而稍微思考一下便知道这显然是一般情况。

然后都有gcd了,发现操作的实质都差不多是将\(a_1\)减去几个\(a_2\)后交换再相减,类似gcd递归版的取模操作,同时ans加上\(\left \lfloor \frac{a_1}{a_2} \right \rfloor\)

最后算出来的数与实际答案差1,大概是0的问题,所以ans加1。

然后试了很多组小数据发现是对的。

最后就AC了。

const int INF=0x7fffffff;

ll ans;

void gcd(ll a,ll b)
{
	if(b==0)
		return;
	ans+=a/b;
	gcd(b,a%b);
}

int main()
{
  freopen("seq.in","r",stdin);
  freopen("seq.out","w",stdout);
	gcd(read<ll>(),read<ll>());
	printf("%lld\n",ans+1);
//  fclose(stdin);
//  fclose(stdout);
    return 0;
}

标解

有个归纳证明的过程。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;

ll a,b,c,ans;
int main(){
freopen("seq.in","r",stdin);
freopen("seq.out","w",stdout);
	scanf("%lld%lld",&a,&b);
	if (a<b) swap(a,b);
	c = a%b;
	while (c){
		ans += a/b;
		a = b;b = c;c = a%b;
	}
	ans += a/b;		
	ans++;
	printf("%lld\n",ans);
	return 0;
}

APIO2016 烟火表演

烟花表演是最引人注目的节日活动之一。在表演中,所有的烟花必须同时爆炸。

为了确保安全,烟花被安置在远离开关的位置上,通过一些导火索与开关相连。导火索的连接方式形成一棵树,烟花是树叶,如图所示。

火花从开关出发,沿导火索移动。每当火花抵达一个分叉点时,它会扩散到与之相连的所有导火索,继续燃烧。导火索燃烧的速度是一个固定常数。

图中展示了六枚烟花 \(\{E_1, E_2, \dots, E_6\}\) 的连线布局,以及每根导火索的长度。图中还标注了当在时刻 \(0\) 从开关点燃火花时,每一发烟花的爆炸时间。

烟火表演_1

Hyunmin 为烟花表演设计了导火索的连线布局。不幸的是,在他设计的布局中,烟花不一定同时爆炸。我们希望修改一些导火索的长度,让所有烟花在同一时刻爆炸。例如,为了让图中的所有烟花在时刻 \(13\) 爆炸,我们可以像下图中左边那样调整导火索长度。类似地,为了让图中的所有烟花在时刻 \(14\) 爆炸,我们可以像下图中右边那样调整长度。

烟火表演_2

修改导火索长度的代价等于修改前后长度之差的绝对值。例如,将上面那副图中布局修改为下面那副图的左边布局的总代价为 \(6\),而修改为右边布局的总代价为 \(5\)

导火索的长度可以被减为 \(0\),同时保持连通性不变。

给定一个导火索的连线布局,你需要编写一个程序,去调整导火索长度,让所有的烟花在同一时刻爆炸,并使得代价最小。

\(1 \leq M \leq 300000\)

题解

看上去是水题,但是发现边权可以减一之后就不是那么容易了。

理论基础

\(f_i(x)\) 表示以第 \(i\) 个点为根的子树调整至叶子需要 \(x\) 秒引爆的代价。我们发现,这个函数是一个下凸的一次分段函数。

考虑这个函数合并到父亲节点时会发生怎样的变化。

\(f_i'(x)\) 是原函数,\(f_i(x)\) 是新函数,\(i\) 和父亲之间的边长度为 \(l\)\([L, R]\)\(f_i'(x)\) 斜率为 \(0\) 的那一段的左右端点的横坐标,那么有:

\[f_i(x) = \begin{cases} f_i'(x) + l, & x \leq L \\ f_i'(L) + (l - (x - L)), & L < x \leq L + l \\ f_i'(x-l), & L + l < x \leq R + l \\ f_i'(L) + ((x - R) - l), & R + l < x \end{cases} \]

我们一个一个来看。

  1. \(x \leq L\) 时,我们肯定要让新的 \(l\) 越小越好。因为改变 \(l\) 的代价为 \(1\),而 \(f_i'\) 这个函数在 \(\leq L\) 的时候斜率 \(\leq -1\),即让 \(x\) 变小的代价 \(\geq 1\),所以干脆将 \(l\) 变成 \(0\)
  2. 我们只要保证 \(x = L\) 就能取到函数的最小值,于是 \(l\) 的变化量越小越好。
  3. 我们不用改变 \(l\) 就可以保证能取到最小值,那就不用改变了。
  4. 同第一个很像,我们要让新的 \(l\) 越大越好。

那么这个过程究竟对这个函数做了什么改变呢?

我们将 \(\leq L\) 部分的函数向上平移了 \(l\) 单位,将 \([L,R]\) 部分向右平移 \(l\) 单位,在 \([L,L+l]\) 部分插入了一条斜率为 \(-1\) 的直线,并将 \(> R + l\) 的部分的斜率改为了 \(1\)

于是大概变成了这个样子:

烟火表演_3

这样,各个拐点之间的直线的斜率是从左到右递增的。

实现细节

我们考虑维护分段函数的拐点

我们不妨假设各个拐点之间的直线斜率的增量为 \(1\),如果有一个斜率不存在,那么我们就用两个同一位置的拐点来表示这个不存在的斜率。

对于叶子节点,有两个拐点 \((0,0),(0,0)\)。这是一棵树,如何将 \(i\) 转移到 \(i\) 父亲 \(fa_i\) 呢?

先说:最右端直线的斜率是 \(i\) 的子节点数(接下来会证明)。

用一个大根堆维护拐点横坐标。先不断弹出最右端斜率大于 \(1\) 的直线,使得最后一段直线的斜率为 \(1\)

然后把斜率为 \(0\) 的直线段对应的两个拐点右移 \(l\) 个单位。把这个大根堆合并到父亲。

而这时候每个点往父亲合并时最右端的斜率一定为 \(1\) ,故 \(i\) 最右端直线的斜率是 \(i\) 的子节点数。利用这个就能判断最右端直线的斜率

那么我们知道每个函数被合并上去之前会变成什么样子了,那么我们也可以非常简单的合并两个函数了,我们只需要将两个函数的拐点列表合并一下就可以了。

我们再看看在合并到父亲节点时要做的操作:

  1. 将斜率 \(> 0\) 的那一段的斜率改为 \(1\)
    因为我们合并上来的函数的斜率最大值都为 \(1\),所以我们只需要删除 \(k - 1\) 个最大的拐点即可,其中 \(k\) 是这个点儿子的数量。
  2. 将斜率 \(=0\) 的那一段平移 \(l\) 单位。
    我们做完操作一之后,横坐标最大的两个拐点就是斜率为 \(0\) 的两个端点了,将它们弹出来,加上 \(l\) 再放进去就没了。
  3. 加入一段斜率为 \(-1\) 的直线。
    这个其实在做操作二的时候就顺带做完了。

我们维护一个可并堆就可以做上面的所有操作。

最后求答案时,我们保留 \(L\) 及其左边的拐点,依次减去它们的横坐标就是我们想要的函数值了。

时间复杂度 \(O(n \log n)\)

#include<bits/stdc++.h>
#include<ext/pb_ds/priority_queue.hpp>
using namespace std;
template<class T> T read(){
    T x=0,w=1;char c=getchar();
    for(;!isdigit(c);c=getchar())if(c=='-') w=-w;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    return x*w;
}
template<class T> T read(T&x){
    return x=read<T>();
}
#define co const
#define il inline
typedef long long LL;

co int N=600000+10;
__gnu_pbds::priority_queue<LL,less<LL>,__gnu_pbds::pairing_heap_tag> rt[N];
int fa[N],C[N],deg[N];

int main(){
	int n=read<int>(),m=read<int>();
	LL ans=0;
	for(int i=2;i<=n+m;++i)
		++deg[read(fa[i])],ans+=read(C[i]);
	for(int i=n+m;i>1;--i){
		LL l=0,r=0;
		if(i<=n){
			while(--deg[i]) rt[i].pop();
			r=rt[i].top(),rt[i].pop();
			l=rt[i].top(),rt[i].pop();
		}
		rt[i].push(l+C[i]),rt[i].push(r+C[i]);
		rt[fa[i]].join(rt[i]);
	}
	while(deg[1]--) rt[1].pop();
	for(;rt[1].size();rt[1].pop()) ans-=rt[1].top();
	printf("%lld\n",ans);
	return 0;
}

pb_ds真好用。

posted on 2019-09-15 21:55  autoint  阅读(161)  评论(0编辑  收藏  举报

导航