1. 程式人生 > >九省聯考2018 林克卡特樹

九省聯考2018 林克卡特樹

Link

Difficulty

演算法難度7,思維難度7,程式碼難度5

Description

給定一棵nn個點的樹,邊帶權值,要求你選出k+1k+1條鏈,使得權值和最大。

1k<n3×105v1061\le k<n\le 3\times 10^5,|v|\le 10^6

Solution

前面的小部分分我就不說了,說一下和正解有極大聯絡的60分的樹形dp吧。

首先我們考慮設計dp狀態。

第一想法是dp(i,j)dp(i,j)代表在ii的子樹中選了jj條鏈的最大價值,看起來非常美好。

但是仔細想想發現沒法寫狀態轉移方程,因為不知道到底能不能和兒子連邊,也不知道連邊會發生什麼事。

這樣我們就發現我們還要記錄一下每個點的連邊狀態。

dp(i,j,0/1/2)dp(i,j,0/1/2)代表在ii的子樹中完整地選了jj條鏈的最大價值,0/1/20/1/2代表點ii的度數。

首先初始狀態:

  1. dp(i,0,0)=0dp(i,0,0)=0,代表這個點可以不選。
  2. dp(i,0,1)=0dp(i,0,1)=0,代表這個點可以作為鏈最下面的點向上連。
  3. dp(i,1,2)=0dp(i,1,2)=0,代表這個點可以單獨作為一條鏈,至於為什麼要有這個狀態,只需要想一下極端情況k+1=nk+1=n時,合法答案是什麼樣子的就可以了。
  4. 其他都為負無窮,也就是不合法

考慮轉移狀態,將兒子uu的狀態合併到點xx:(i:k1i:k\to 1代表iikk列舉到11,下面不再描述)

  1. dp(x,i,0)=max(dp(x,i,0),dp(x,ij,0)+dp(u,j,0))dp(x,i,0)=max(dp(x,i,0),dp(x,i-j,0)+dp(u,j,0)),其中i:k1,j:1ki:k\to 1,j:1\to k

    代表不選或者選jj條鏈。

  2. dp(x,i,1)=max(dp(x,i,1),dp(x,ij,1)+dp(u,j,0)

    ,dp(x,ij,0)+dp(u,j,0)+val(x,u))dp(x,i,1)=max(dp(x,i,1),dp(x,i-j,1)+dp(u,j,0),dp(x,i-j,0)+dp(u,j,0)+val(x,u)),其中i:k1,j:1ki:k\to 1,j:1\to k

    代表不選,選jj條鏈,或者選jj條鏈並且選這條邊。

  3. dp(x,i,2)=max(dp(x,i,2),dp(x,ij,2)+dp(u,j,0),dp(x,ij,1)+dp(u,j1,1)+val(x,u))dp(x,i,2)=max(dp(x,i,2),dp(x,i-j,2)+dp(u,j,0),dp(x,i-j,1)+dp(u,j-1,1)+val(x,u)),其中i:k1,j:1ki:k\to 1,j:1\to k

    代表不選,選jj條鏈,或者選j1j-1條鏈並且選這條邊增加一條鏈。

  4. dp(x,i,1)=max(dp(x,i,1),dp(x,i,0)+dp(u,0,0)+val(x,u))dp(x,i,1)=max(dp(x,i,1),dp(x,i,0)+dp(u,0,0)+val(x,u)),其中ik1i:k\to 1

    這個看起來跟上面的第二個轉移的重複了,事實上並沒有,因為這個轉移既合法,第二個轉移又轉移不到。

  5. dp(x,0,1)=max(dp(x,0,1),dp(u,0,1)+val(x,u))dp(x,0,1)=max(dp(x,0,1),dp(u,0,1)+val(x,u))

    代表從下面連上來,同樣是第二個轉移沒有轉移到的。

  6. dp(x,i,0)=max(dp(x,i,0),dp(x,i1,1),dp(x,i,2))dp(x,i,0)=max(dp(x,i,0),dp(x,i-1,1),dp(x,i,2)),其中i1ki:1\to k

    代表不選,在這裡停止這條鏈並計入總數,或者把那兩個度數去掉。

轉移方程大概就是這些了,dp的順序呀,細節呀,就看我的程式碼吧。

這樣的話複雜度有些玄學(調迴圈邊界的話),我不太會算,反正只能有4545分,會TLE。

本來想把這個dp放到dfs序上說不定就可以到O(nk)O(nk)了,後來發現我不會QAQ

這個dp必須先寫一下,因為凸優化的程式碼就是在dp的基礎上改的。

拿到45分之後,我們來看這題正解吧。

凸優化

凸優化就是針對凸函式求極值的優化。

我們這裡不直接探究它的定義及一般情況,我們直接來看這個題,通過這個題來理解凸優化。

首先,通過打表可以發現,答案的函式是上凸的,對於樣例來說畫出來是這樣的:

雖然影象有點兒尖,但是它確實是上凸的。

怎麼直接判斷一個題的答案是否上凸呢?

我們可以感性判斷,比如對於這個題,假如只能選一條鏈的話,一定是選最長的,選兩條的話,增長的就沒有第一條那麼多了,因為最長的已經選過了,這樣來看,增長只會越來越慢,所以它是凸函式。

現在我們知道它是凸函數了,應該怎麼做呢?

我們二分一個權值midmid,代表選一條鏈需要付出的代價,然後我們去掉選多少條鏈那一維,還按照原來的dp做。

這樣子相當於我們拿y=mid×xy=mid\times x的直線去切答案函式,在這個基礎上求極值。

但是我們發現這樣求得極值之後,無法判斷下一次midmid變小還是變大。

我們同樣可以發現,切了之後的可以取得極值的點是一段連續的區間。

因此,在此基礎上我們再記錄取得極值的最小的kk是多少,也就是區間的左端點是多少。

假如題目中的kk等於左端點的話,直接輸出答案。

假如題目中的kk一定不在這個區間內(左端點大於kk),則令l=mid+1l=mid+1,讓選的代價變大,左端點減小。

假如題目中的kk有可能在這個區間內(左端點小於kk),則令r=midr=mid,讓選的代價變小,左端點增大。

最後令mid=lmid=l,再做一次得到最終答案,並且把那個選的代價加回來,就好了。

感性理解一下這個過程,感覺挺對的QAQ

然後這個做法就叫凸優化啦,是不是感覺也沒什麼難的?

時間複雜度O(nlogV)O(nlogV),還有樹形dp常數挺大,所以跑得比較慢。

#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
#define LL long long
using namespace std;
inline int read(){
    int x=0,f=1;char ch=' ';
    while(ch<'0' || ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0' && ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return f==1?x:-x;
}
const int N=3e5+5,K=105;
const LL inf=1e18;
int n,k,tot;
int head[N],to[N<<1],Next[N<<1],val[N<<1];
struct data{
    LL x,y;
    data(){}
    data(LL _x,LL _y):x(_x),y(_y){}
    inline bool operator < (const data& b) const {
        if(x==b.x)return y>b.y;
        return x<b.x;
    }
    inline data operator + (const data& b) const {return data(x+b.x,y+b.y);}
    inline data operator + (LL b) const {return data(x+b,y);}
}dp[N][3];
inline void addedge(int x,int y,int l){
    to[++tot]=y;
    Next[tot]=head[x];
    head[x]=tot;
    val[tot]=l;
}
LL mid;
inline void dfs(int x,int fa){
    dp[x][0]=data(0,0);
    dp[x][1]=data(0,0);
    dp[x][2]=max(data(0,0),data(-mid,1));
    for(int i=head[x];i;i=Next[i]){
        int u=to[i];
        if(u==fa)continue;
        dfs(u,x);
        dp[x][2]=max(dp[x][2],max(dp[x][2]+dp[u][0],dp[x][1]+dp[u][1]+val[i]+data(-mid,1)));
        dp[x][1]=max(dp[x][1],max(dp[x][1]+dp[u][0],dp[x][0]+dp[u][1]+