1. 程式人生 > 其它 >[學習筆記] 決策單調性與 wqs 二分

[學習筆記] 決策單調性與 wqs 二分

一看時間,這已經是 2020 年的草稿了…… 不管了,咕咕咕~ 目錄

決策單調性

四邊形不等式

對於 \(\mathtt{dp}\) 轉移的花費(\(a\le b\le c\le d\)),首先要滿足這樣的柿子:

\[w(a,c)+w(b,d)\le w(a,d)+w(b,c) \]

注意,符號也可以取 "\(\ge\)",這個視題目要求而定。另外注意 \(w(x,y)\) 的定義是 "從 \(x\) 轉移到 \(y\) 的花費"。

決策單調

\(\mathtt{dp}\) 轉移到

\[dp_i=\min\{dp_j+w(j,i)\} \]

為例。假設 \(w\)

滿足條件 "\(\le\)"。

令點 \(i\) 的決策點為 \(p_i\),首先需要 預設 \(p_i \le i\)

那麼有:

\[dp_{p_i}+w(p_i,i)\le dp_j+w(j,i)\\ dp_{p_{i+1}}+w(p_{i+1},i+1)\le dp_j+w(j,i+1) \]

\(p_{i+1}\ge i\),那麼有 \(p_{i+1}\ge p_i\),得證。

反之,可以把柿子替換成

\[dp_{p_i}+w(p_i,i)\le dp_{p_{i+1}}+w(p_{i+1},i)\\ dp_{p_{i+1}}+w(p_{i+1},i+1)\le dp_{p_i}+w(p_i,i+1) \]

將兩式相加,得到:

\[w(p_i,i)+w(p_{i+1},i+1)\le w(p_{i+1},i)+w(p_i,i+1) \]

套用四邊形不等式,就可以得到 \(p_i\le p_{i+1}\),得證。

套路

  • \(p_i\le p_{i+1}\):"\(\le\)" 且 \(\min\);"\(\ge\)" 且 \(\max\)
  • \(p_i\ge p_{i+1}\):"\(\ge\)" 且 \(\min\);"\(\le\)" 且 \(\max\)

\(\text{wqs}\) 二分

[國家集訓隊]\(\text{ Tree I}\)

解法

\(g(m)\) 為選 \(m\) 條白邊得到最小生成樹的權值。這個函式是下凸的,具體可以這樣感性證明 —— 設選 \(k\)

條白邊時正好得到無限制的最小生成樹,那麼往右/左選都只可能用不優的白邊替換黑邊;或用不優的黑邊替換白邊。

考慮 \(\text{wqs}\) 二分。二分斜率為 \(\rm mid\) 的直線切那個凸包,顯然切點就是截距最小的點,令 \(f(m)=g(m)-m\cdot \rm mid\),將所有白邊減去 \(\rm mid\),求出最小生成樹,其對應的權值即為 \(f(m)\),記錄選出的白邊 \(m'\)。由於斜率增大時,選擇白邊數增大,可以依據此調整斜率。

但是會不會出現這樣的情況:二分 \(\text{mid},\text{mid}+1\) 時,得到的白邊數分別為 \(x,x+\delta(\delta >1)\),這樣就會忽略掉 \(x+\Delta=\text{need}(1\le \Delta <\delta)\) 的情況。

事實上,只要當白邊黑邊權值相等時選擇白邊就可以保證求出最小生成樹答案正確。假設面對上文的情況,最終得到的二分值為 \(\rm mid\)。考慮增加 \(\delta\) 條白邊滿足什麼條件:在 \(\text{mid}\) 狀態下,有 \(\delta\) 條白邊的權值為 \(\delta\) 條不同黑邊的權值 \(+1\)。此時一定可以將白邊用黑邊替換,從而得到 \(\rm need\) 條白邊。這種 "替換" 這也是輸出方案是 \(\rm wqs\) 二分的瓶頸的原因。

實現可以使用雙指標,複雜度 \(\mathcal O(m\log(m+V))\)

程式碼

#include <cstdio>

#define rep(i,_l,_r) for(register signed i=(_l),_end=(_r);i<=_end;++i)
#define fep(i,_l,_r) for(register signed i=(_l),_end=(_r);i>=_end;--i)
#define erep(i,u) for(signed i=head[u],v=to[i];i;i=nxt[i],v=to[i])
#define efep(i,u) for(signed i=Head[u],v=to[i];i;i=nxt[i],v=to[i])
#define print(x,y) write(x),putchar(y)

template <class T> inline T read(const T sample) {
    T x=0; int f=1; char s;
    while((s=getchar())>'9'||s<'0') if(s=='-') f=-1;
    while(s>='0'&&s<='9') x=(x<<1)+(x<<3)+(s^48),s=getchar();
    return x*f;
}
template <class T> inline void write(const T x) {
    if(x<0) return (void) (putchar('-'),write(-x));
    if(x>9) write(x/10);
    putchar(x%10^48);
}
template <class T> inline T Max(const T x,const T y) {if(x>y) return x; return y;}
template <class T> inline T Min(const T x,const T y) {if(x<y) return x; return y;}
template <class T> inline T fab(const T x) {return x>0?x:-x;}
template <class T> inline T gcd(const T x,const T y) {return y?gcd(y,x%y):x;}
template <class T> inline T lcm(const T x,const T y) {return x/gcd(x,y)*y;}
template <class T> inline T Swap(T &x,T &y) {x^=y^=x^=y;}

#include <iostream>
#include <algorithm>
using namespace std;

const int maxn=5e4+5,maxm=1e5+5;

int n,m,Need,cnt,tot,sum,f[maxn],MinSum;
struct Edge {
	int u,v,w;
	
	bool operator < (const Edge t) const {
		return w<t.w;
	}
} e[maxm];

bool init() {rep(i,1,n) f[i]=i;}

int Find(int x) {return x==f[x]?x:f[x]=Find(f[x]);}

void addEdge(int i,int w) {
	int x=Find(e[i].u),y=Find(e[i].v);
	if(x^y) {
		sum+=w,f[x]=y;
		if(i<=cnt) ++tot;
	}
}

int main() {
	n=read(9),m=read(9),Need=read(9);
	rep(i,1,m) {
		e[i].u=read(9)+1,e[i].v=read(9)+1,e[i].w=read(9);
		if(!read(9)) swap(e[++cnt],e[i]);
	}
	sort(e+1,e+cnt+1),sort(e+cnt+1,e+m+1); 
	int l=-100,r=100,mid,p,q;
	while(l<r) {
		mid=l+r+1>>1;
		init(),sum=tot=p=0,q=cnt;
		while(p<cnt && q<m)
			if(e[p+1].w+mid<=e[q+1].w) ++p,addEdge(p,e[p].w+mid);
			else ++q,addEdge(q,e[q].w);
		while(p<cnt) ++p,addEdge(p,e[p].w+mid);
		while(q<m) ++q,addEdge(q,e[q].w);
		if(tot<Need) r=mid-1;
		else MinSum=sum,l=mid;
	}
	print(MinSum-l*Need,'\n');
	return 0;
}

[八省聯考 2018] 林克卡特樹

題目描述

割掉 \(k\) 條邊之後會出現 \(k+1\) 個聯通塊,那麼對於每一個聯通塊求直徑再用 \(k\) 條邊將這 \(k+1\) 個聯通塊串起來即可,所以問題轉化為:給定一棵 \(n\) 個點的樹,邊權有正有負,要求在樹上選出 \(k+1\) 條鏈,使得其權值之和最大。

注意,單個節點也是一條鏈,它的權值為零。

解法

首先進行一個 樸素 \(\mathtt{dp}\)

\(g(m)=dp_{1,m,0}\),則 \((m,g(m))\) 形成一個上凸包。為啥捏?考慮上凸滿足條件為 \(g(i)-g(i-1)\ge g(i+1)-g(i)\),考慮每增加一條鏈一定優先選擇更優的。當鏈的個數足夠多時,有些鏈被劃分成點,就會丟失邊權;或者增加負邊權鏈。而且我們發現 \(g(0)=g(n)=0\)(全是點就無邊權了),一定程度上證實了這個猜想。

\(\rm wqs\) 二分過程就略過了,和上面是一樣的~ 需要注意 \(\mathtt{dp}\) 過程中還需要求出最小權值對應的鏈數。

如果凸包上有點共線咋辦?這個沒有關係,可以欽定選最左/右的點,我們需要的只是斜率。

另外就是 \(\rm mid\) 為什麼是整數的問題。考慮問題就是在 \(g\) 的凸包上找切點 \((m,g(m))\) 的問題,由於 \(g\) 為整數,所以經過 \((m,g(m)),(m+1,g(m+1))\) 的直線斜率一定是整數。

程式碼

#include <cstdio>

#define rep(i,_l,_r) for(register signed i=(_l),_end=(_r);i<=_end;++i)
#define fep(i,_l,_r) for(register signed i=(_l),_end=(_r);i>=_end;--i)
#define erep(i,u) for(signed i=head[u],v=to[i];i;i=nxt[i],v=to[i])
#define efep(i,u) for(signed i=Head[u],v=to[i];i;i=nxt[i],v=to[i])
#define print(x,y) write(x),putchar(y)

template <class T> inline T read(const T sample) {
    T x=0; int f=1; char s;
    while((s=getchar())>'9'||s<'0') if(s=='-') f=-1;
    while(s>='0'&&s<='9') x=(x<<1)+(x<<3)+(s^48),s=getchar();
    return x*f;
}
template <class T> inline void write(const T x) {
    if(x<0) return (void) (putchar('-'),write(-x));
    if(x>9) write(x/10);
    putchar(x%10^48);
}
template <class T> inline T Max(const T x,const T y) {if(x>y) return x; return y;}
template <class T> inline T Min(const T x,const T y) {if(x<y) return x; return y;}
template <class T> inline T fab(const T x) {return x>0?x:-x;}
template <class T> inline T gcd(const T x,const T y) {return y?gcd(y,x%y):x;}
template <class T> inline T lcm(const T x,const T y) {return x/gcd(x,y)*y;}
template <class T> inline T Swap(T &x,T &y) {x^=y^=x^=y;}

#include <iostream>
using namespace std;
typedef long long ll;

const int maxn=3e5+5;

int n,k;
int cnt,head[maxn],nxt[maxn<<1],to[maxn<<1],val[maxn<<1];
struct node {
	ll val; int num;
	
	friend bool operator < (const node x,const node y) {
		return (x.val^y.val)?x.val<y.val:x.num>y.num;
	}
	
	friend node operator + (const node x,const node t) {
		return (node){x.val+t.val,x.num+t.num};
	}
	
	friend node operator + (const node x,const int t) {
		return (node){x.val+t,x.num};
	}
} f[maxn][3],e;

void addEdge(int u,int v,int w) {
	nxt[++cnt]=head[u],to[cnt]=v,val[cnt]=w,head[u]=cnt;
	nxt[++cnt]=head[v],to[cnt]=u,val[cnt]=w,head[v]=cnt;
}

void dfs(int u,int fa) {
	f[u][0]=f[u][1]=f[u][2]=(node){0,0};
	erep(i,u) {
		if(v==fa) continue;
		dfs(v,u);
		f[u][2]=max(f[u][2],max(f[u][1]+f[v][1]+val[i]+e,f[u][2]+f[v][0]));
		f[u][1]=max(f[u][1],max(f[u][1]+f[v][0],f[u][0]+f[v][1]+val[i]));
		f[u][0]=max(f[u][0],f[u][0]+f[v][0]);
	}
	f[u][0]=max(f[u][0],max(f[u][2],f[u][1]+e));
}

int main() {
	n=read(9),k=read(9)+1;
	rep(i,1,n-1) {
		int u=read(9),v=read(9),w=read(9);
		addEdge(u,v,w);
	}
	ll l=-1e12,r=1e12,mid;
	while(l<r) {
		mid=(l+r)>>1;
		e=(node){-mid,1};
		dfs(1,0);
		if(f[1][0].num==k) return print(f[1][0].val+1ll*mid*k,'\n'),0;
		if(f[1][0].num>k) l=mid+1;
		else r=mid;
	}
	e=(node){-l,1};
	dfs(1,0);
	print(f[1][0].val+1ll*l*k,'\n');
	return 0;
}