1. 程式人生 > 其它 >[提高組集訓2021] Round2

[提高組集訓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);
}