1. 程式人生 > >dp凸優化/wps二分學習筆記(洛谷4383 [八省聯考2018]林克卡特樹lct)

dp凸優化/wps二分學習筆記(洛谷4383 [八省聯考2018]林克卡特樹lct)

qwq
安利一個凸優化講的比較好的部落格

https://www.cnblogs.com/Gloid/p/9433783.html

但是他的暴力部分略微有點問題
qwq

我還是詳細的講一下這個題+這個知識點吧。

還是先從題目入手。

首先我們分析題目。
因為題目要刪除 k k 條邊,然後再新建 k

k 條邊,求兩點的路徑和。
那我們不妨這麼考慮,對於新連線一條邊,相當於連結了原樹上的兩條鏈,且鏈不存在交點。
那我們新建 k k 條邊,就相當於把原樹上沒有交的 k + 1
k+1
條鏈連線起來。
既然要求權值最大。

那我們就可以直接把題目轉換成求樹上選出點不相交的 k + 1 k+1 條鏈的最大收益。(每個點都是一條鏈)。

qwq
首先考慮應該怎麼

暴力 d
p dp

由於對於 x x 這顆子樹,要分情況討論他屬於一條鏈的端點,中間點,還是不屬於鏈。

所以我們定義狀態 d p [ i ] [ j ] [ 0 / 1 / 2 ] dp[i][j][0/1/2] 表示 i i 的子樹裡,已經選了 j j 條鏈,其中 i i 這個點不屬於鏈,屬於一個鏈的端點,屬於一個鏈的中心點的最大收益。

首先,因為負權,所以要把所有的 d p dp 弄成初始值是 i n f -inf 的,其中 d p [ i ] [ 0 ] [ 0 ] dp[i][0][0] 為0。

然後我們考慮應該怎麼轉移。
對於一個點來說,我們先列舉他的兒子,然後列舉他子樹內的鏈數 j j ,列舉分配給他的兒子的鏈數 z z

那麼對於不同的 0 / 1 / 2 0/1/2 我們需要分情況討論。
對於 0 0 來說,他可以從兒子的任意一個狀態轉移過來。
對於 1 1 來說,首先他可以由當前狀態的 1 1 +兒子的 0 / 1 / 2 0/1/2 中最大的那個轉移,表示不與當前的兒子構成鏈。
也可以從當前狀態的0+兒子的1+ v a l [ i ] val[i] ,表示從當前兒子上來一條鏈。
另外的一種情況就是隻選與當前兒子相連的邊。

對於2來說,其實也是和1同理。

void solve(int x,int fa)
{	
  for (int i=point[x];i;i=nxt[i])
  {
    int p = to[i];
    if(p==fa) continue;
    solve(p,x);
    for (int j=min(k,size[x]);j>=1;j--)
    {
	  for (int z=0;z<=min(j,size[p]);z++)
	  {
	     dp[x][j][2]=max(dp[x][j][2],dp[x][j-z][2]+max(dp[p][z][0],max(dp[p][z][1],dp[p][z][2])));
		 dp[x][j][2]=max(dp[x][j][2],dp[x][j-z][1]+dp[p][z+1][1]+val[i]);
		 if (j>z) dp[x][j][2]=max(dp[x][j][2],dp[x][j-z][1]+val[i]+dp[p][z][0]);
	  	 //本身就有1一條鏈 
		 dp[x][j][1]=max(dp[x][j][1],dp[x][j-z][1]+max(dp[p][z][0],max(dp[p][z][1],dp[p][z][2])));
	  	 //和當前構成一條鏈
		 dp[x][j][1]=max(dp[x][j][1],dp[x][j-z][0]+dp[p][z][1]+val[i]);
		 if (j>z) dp[x][j][1]=max(dp[x][j][1],dp[x][j-z-1][0]+dp[p][z][0]+val[i]); 
		 dp[x][j][0]=max(dp[x][j][0],dp[x][j-z][0]+max(dp[p][z][0],max(dp[p][z][1],dp[p][z][2]))); 
      }
	}
  }
}

這裡有兩個需要注意的問題,首先是 j j 要倒著列舉,因為一個鏈只能被選一次(防止出現舊自己更新新自己的情況)
其次 z z 要迴圈到0(只是用來針對只算一條邊的情況。)

然後通過以上的過程,我們就能輕鬆愉悅的通過 O ( n k ) O(nk) 的60分了。
那麼應該怎麼優化這個過程呢。

凸優化

首先,我們通過做差分,即 a n s [ i ] [ j ] a n s [ i ] [ j 1 ] ans[i][j]-ans[i][j-1] a n s [ i ] [ j ] ans[i][j] 表示 n = i , k = j n=i,k=j 時候的答案,發現斜率是逐漸降低的,那我們不難發現在 i i 一定的情況下,其實 a n s ans 是關於 j j 的上凸函式。(並且一定要滿足斜率單調!!!!!!)、

那我們實際上就是要求出來在 j = k j=k 的時候的那個對應的凸包的點是多少。
根據斜率單調
我們可以直接二分一個 m i d mid ,然後讓所有的邊權都加上 m i d mid ,然後不限制選的鏈的條數,求最大收益和鏈數,這個複雜度是 O ( n ) O(n) 的。

因為我們發現,當 m i d = i n f mid= inf 的時候,一定是選 n n 條,當 i n f = m i d inf=-mid 的時候,是選0條,那麼因為斜率單調(所以選擇的條數一定是隨著mid單調變大的),所以我們一定能通過改變這個 m i d mid ,存在某一個 m i d mid 滿足恰好選擇 k k 條,那麼正好符合題目要求。

另外一種理解方式。

qwq這個理解方式,每次要減去 m i d mid ,其實本質是一樣的。
我們相當於對於每個點求出他的正上方所對應的凸包的點是啥,那麼我們通過 e r f erf 這個東西,相當於把原圖的上的每個點 x x 向下移動 m i d x mid*x ,那麼我們可以發現,隨著 m i d mid 的變大,每次隨便選的取到的收益的最高的地方,是單調右移的。我們只需要記錄一下選的最大收益和鏈數,然後找到鏈數等於 k k 所對應的 m i d mid ,計算一遍貢獻就行。

至於如何做不限制條數的收益,和 n k nk 類似的 d p dp

感覺這個東西要感性+理性啊

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#include<map>
#include<set>
#define mk make_pair
#define ll long long
#define int long long
using namespace std;
inline int read()
{
  int x=0,f=1;char ch=getchar();
  while (!isdigit(ch)) {if (ch=='-') f=-1;ch=getchar();}
  while (isdigit(ch)) {x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}
const int maxn = 3e5+1e2;
const int maxm = 2*maxn;
const int inf = 1e12;
int point[maxn],nxt[maxm],to[maxm],val[maxm];
int n,m,cnt;
int l,r,ans,k;
struct ymh{
	int val,num;
	ymh operator + (ymh b)
	{
		return (ymh){val+b.val,num+b.num};
	}
}; 
ymh max(ymh a,ymh b)
{
	if(a.val==b.val) 
	{
		if(a.num<b