[提高组集训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\) 的糖果的最小花费,不难写出如下转移:
其实 \((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);
}