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