[提高组集训2021] Round2

懒得说废话了,我是傻逼。

C

题目描述

给定一棵 \(n\) 个点的树,记 \(L(u,v)\)\((u,v)\) 简单路径上的点数。对于路径 \((a,b),(c,d)\) 点不交的四元组 \((a,b,c,d)\),我们想知道 \((L(a,b),L(c,d))\) 有多少种不同的取值。

\(n\leq 5\cdot 10^5\)

解法

关键的 \(\tt observation\) 是:对于任意的四元组,一定存在一条边使得两条路径分别在两个子树内。

那么我们可以枚举这条边,两个子树内都取直径即可,发现可以用换根 \(dp\) 实现这个过程,然后需要做一个矩形面积并,其实就是一个后缀最大值啦。

总结

这题有一个"不交"的限制,我们可以枚举一个东西把它们"割开"。

#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
const int M = 500005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,lf[M],b[M];vector<int> g[M];
struct diameter
{
	int x,y,z;
	diameter() {x=y=z=0;}
	void add(int c)
	{
		if(c>x) z=y,y=x,x=c;
		else if(c>y) z=y,y=c;
		else if(c>z) z=c;
	}
	int len(int ban)
	{
		if(x==ban) return y+z+1;
		if(y==ban) return x+z+1;
		if(z==ban) return x+y+1;
		return x+y+1;
	}
}dp[M];
void upd(int &x,int y) {x=max(x,y);}
void work(int u,int fa)
{
	for(auto v:g[u]) if(v^fa)
		work(v,u),dp[u].add(dp[v].x+1);
}
void dfs(int u,int fa,int mx)
{
	dp[u].add(lf[u]);
	for(auto v:g[u]) if(v^fa)
	{
		int ban=dp[v].x+1,wv=dp[v].len(0);
		int wu=max(dp[u].len(ban),mx);
		upd(b[wu],wv);upd(b[wv],wu);
		lf[v]=(dp[u].x==dp[v].x+1)?dp[u].y+1:dp[u].x+1;
		dfs(v,u,wu);
	}
}
signed main()
{
	freopen("tree.in","r",stdin);
	freopen("tree.out","w",stdout);
	n=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u);
	}
	work(1,0);dfs(1,0,0);
	long long ans=0;
	for(int i=n;i>=1;i--)
	{
		b[i]=max(b[i],b[i+1]);
		ans+=b[i];
	}
	printf("%lld\n",ans);
}

D

题目描述

\(n+1\) 个城市在一条线上,从左往右从 \(0\) 开始标号。其中第 \(i\) 个城市距离城市 \(0\) 的距离为 \(a_i\),你要从 \(0\) 号城市开始出发到达城市 \(n\) , 你每经过一单位的距离需要吃一颗糖。

每个城市有一个糖果店,有无限多的糖,第 \(i\) 个城市的糖果店买一个糖果的价格是 \(b_i\),卖一个糖果的价格是 \(s_i\),你可以在商店里出售多余的糖果。你最多能同时携带 \(m\) 单位糖果。

一开始你有无限多的钱,问最少需要消耗多少的钱可以到达终点(可以是负数)

\(n\le 200000\)

解法1

这种买卖问题的常用套路是:我们寻找一些不合常理的等价操作,技巧常常是延迟

对于本题,我们在每个商店都把背包填满,如果最后有多的糖果我们把它们按原价退钱。根据这个基本思路,我们访问到商店 \(i\) 时进行如下贪心:

  • 首先考虑卖出已有的糖果,设它的价格是 \(x\),该商店的卖出价值是 \(y\),那么卖出它可以赚 \(y-x\),但是暴力卖出是不优,因为后面可能要吃这个糖果,所以我们把它的价值改成 \(y\),那么以后退钱的时候相当于选择了卖出这个糖果,如果吃掉了这个糖果相当于不卖出,这就是高妙的延迟操作。
  • 然后考虑装满背包,可以替换掉一些价格比它大的糖果,然后把当前的糖果塞进去。
  • 最后考虑下一次位移消耗的糖果,根据贪心我们消耗价格最小的糖果。

根据操作特性我们可以用双端队列来维护它,每个元素是一个价值数量的二元组,操作变成:

  • 在队首弹出一些 \(x\) 价值的元素,修改它们的权值为 \(y\)
  • 在队尾弹出一些价格比当前大的元素,插入新元素。
  • 从队首依次删除元素。

根据均摊原理时间复杂度 \(O(n)\)

解法2

这道题也可以从 \(dp\) 的角度入手,只是如果你一直想去降维就会进入思维的死胡同。

\(dp[i][j]\) 表示走到第 \(i\) 个商店还有 \(j\) 的糖果的最小花费,不难写出如下转移:

\[dp[i][j]\leftarrow dp[i-1][j-d] \ \ (1) \]

\[dp[i][j]\leftarrow dp[i][j-k]+k\cdot b_i \ \ (2) \]

\[dp[i][j]\leftarrow dp[i][j+k]-k\cdot s_i \ \ (3) \]

其实 \((2)(3)\) 都是凸函数的合并,而 \((1)\) 就是函数的整体平移,所以这就是一个slope trick的变式,我们可以归纳地证明 \(dp[i]\) 就是一个凸包,转移可以这样翻译成凸包上的操作:

  • 弹出前面的几个点,然后整体平移。
  • 对于后面斜率大于 \(b_i\) 的折线,把它们的斜率改成 \(b_i\)
  • 对于前面斜率小于 \(s_i\) 的折线,把它们的斜率改成 \(s_i\)

这个可以用双端队列实现,然后你发现它和解法一殊途同归了,时间复杂度 \(O(n)\)

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 200005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,l,r,a[M],b[M],s[M],qv[M<<1],qn[M<<1];
long long ans;
signed main()
{
	freopen("candy.in","r",stdin);
	freopen("candy.out","w",stdout);
	n=read();m=read();l=n;r=n-1;
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=0;i<n;i++) b[i]=read(),s[i]=read();
	for(int i=0;i<n;i++)
	{
		//sell the candy
		int cnt=0;
		while(l<=r && qv[l]<=s[i]) cnt+=qn[l],l++;
		qn[--l]=cnt;qv[l]=s[i];
		//fulfill the bagpack & abandon the trash
		cnt=(i==0)?m:a[i]-a[i-1];
		while(l<=r && qv[r]>=b[i])
			ans-=1ll*qn[r]*qv[r],cnt+=qn[r],r--;
		qn[++r]=cnt;qv[r]=b[i];
		ans+=1ll*cnt*b[i];
		//use the candy for walking
		cnt=a[i+1]-a[i];
		while(cnt)
		{
			int v=min(cnt,qn[l]);
			cnt-=v;qn[l]-=v;
			if(qn[l]==0) l++;
		}
	}
	for(int i=l;i<=r;i++) ans-=1ll*qn[i]*qv[i];
	printf("%lld\n",ans);
}
posted @ 2021-11-11 17:19  C202044zxy  阅读(104)  评论(0编辑  收藏  举报