ENSENSO 3D雙目相機垂直測量規則物體點雲體積
動態 \(DP\)
引入:
一般來說,樹上 \(DP\) 問題是不做改變的,只用計算一遍就行了。
但是給你說:更改一個點的權值,再去詢問你答案。
這時候再算一遍?或者是隻處理這個點的父親部分?但是這樣萬一成鏈,那不是時間複雜度又爆了?
所以,我們引入了 動態 \(DP\) 這個概念。
解決問題:
直接說怎麼做不好說,還是用例子解決。
例題:
給定一棵 \(n\) 個點的樹。\(i\) 號點的點權為 \(a_i\)。有 \(m\) 次操作,每次操作給定 \(u,w\) 表示修改點 \(u\) 的權值為 \(w\)。你需要在每次操作之後求出這棵樹的最大權獨立集的權值大小。
考慮沒有修改做法:
最大權獨立集可以理解成相連的點不能同時選,因此很容易轉移:設 \(dp[i][0/1]\)
其中若 \(i\) 為葉子節點,則 \(dp[i][0]=0,dp[i][1]=val[i]\) 。
答案就是 \(max(dp[1][0],dp[1][1])\).
帶上修改
這裡,我們使用了重鏈剖分。因為有以下性質:
- 不會形成特別長的鏈,時間複雜度不會炸
- 重鏈的鏈尾都是葉子節點,且只有葉子節點無重兒子
- 一條重鏈所在的區間在 \(dfs\) 序上是連續的一段區間,可以用資料結構維護。
我們考慮一些微觀問題:在一條鏈裡,怎麼支援快速修改和查詢這條鏈的 \(DP\) 值 。
我們定義一個 \(g\) 陣列:
\(g[i][1]\) 表示 \(i\) 點的輕兒子都不取的最大權獨立集
\(g[i][0]\) 表示 \(i\) 點的輕兒子可取可不取形成的最大權獨立集。
其中,對於葉子節點 \(g[i][0]=g[i][1]=0\) , \(j\) 為 \(i\) 的重兒子,則有方程:
$$dp[i][0]=g[i][0]+max(dp[j][0],dp[j][1])$$
\[dp[i][1]=g[i][1]+a_i+dp[j][0] \]第二個式子繼續優化: \(g[i][1]\) 表示只考慮輕兒子取自己的最大權獨立集,所以變成:
$$dp[i][1]=g[i][1]+dp[j][0]$$
進行區間維護
我們考慮像斐波那契一樣,維護這個方程的矩陣:
\[\quad \begin{vmatrix} dp[j][0] & dp[j][1] \end{vmatrix} \quad \]現在我們要從一個點的重兒子 \(j\) 轉移到 \(i\) 上,也就是說我們需要構造出一個轉移矩陣使: \( \quad \begin{vmatrix} dp[j][0] & dp[j][1] \end{vmatrix} \quad \) 能轉移到 \( \quad \begin{vmatrix} dp[i][0] & dp[i][1] \end{vmatrix} \quad \)
好像並不能進行矩陣乘法。 但是,我們可以重新定義:
《他改變了矩乘》: 我們定義一個新的運算子 \(*\) ,對於矩陣 \(A,B\) ,定義 \(A*B\) 的結果為 \(C\) ,滿足:
\[C[i][j]=\max\limits_k (A[i][k]+B[k][j]) \]實現到程式碼中,則有:
struct matrix{
int mat[2][2];
matrix(){memset(mat,-0X3F,sizeof(mat));}
inline matrix operator *(matrix a,matrix b){
matrix c;
for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
c.mat[i][j]=max(c.mat[i][j],a.mat[i][k]+b.mat[k][j]);
return c;
}
};
為什麼這個東西具有結合律?
口胡理解是: \(\max\) 和 \(+\) 操作都是滿足結合律的,所以這個也滿足。
構造轉移矩陣:
我們對轉移方程進行變形:
\(dp[i][0]=max(dp[j][0]+g[i][0],dp[j][1]+g[i][0])\)
\(dp[i][1]=max(dp[j][0]+g[i][1],-inf)\)
接著把已知的狀態和要轉移的狀態寫在一起,未知矩陣由 \(X\) 代替:
\[\quad \begin{vmatrix} dp[j][0] & dp[j][1] \end{vmatrix} \quad ∗ \quad \begin{vmatrix} X \end{vmatrix} \quad = \quad \begin{vmatrix} dp[i][0] & dp[i][1] \end{vmatrix} \quad \]原來是個 \(1*2\) 的矩陣,形成 \(1*2\) 的矩陣,那麼 \(X\) 應該是一個 \(2*2\) 的矩陣。
我們設矩陣左上,右上,左下,右下四個位置分別為: \(x_1,x_2,x_3,x_4\) ,把每個位置對應上去:
\(dp[i][0]=\max(dp[j][0]+x_1,dp[j][1]+x_3)\) , 所以 \(x_1=g[i][0]\) , \(x_3=g[i][0]\)
\(dp[i][1]=\max(dp[j][0]+x_2,dp[j][1]+x_4)\) , 所以 \(x_2=g[i][1]\) , \(x_4=-inf\)
重鏈剖分剖出的 \(DFS\) 序,由於先訪問了鏈頭,所以這個區間中,鏈頭在區間左端,鏈尾在區間右端。
我們儲存的初始資訊在葉子節點(也就是鏈尾)上,因此我們的 “矩陣乘法” 應當是轉移矩陣 \((g)\) 在前,要維護的值矩陣 \((dp)\) 在後。
因此,則有:
\[\quad \begin{vmatrix} g[i][0] & g[i][0] \\ g[i][1] & -inf \end{vmatrix} \quad ∗ \quad \begin{vmatrix} dp[j][0] \\ dp[j][1] \end{vmatrix} \quad = \quad \begin{vmatrix} dp[i][0] \\ dp[i][1] \end{vmatrix} \quad \]總體流程:
我們對於一條重鏈,我們的葉子節點就儲存了最初始的值,鏈上每個節點都對應著一個轉移矩陣。
因為這個轉移矩陣滿足結合率,且和重鏈資訊沒關係,對於一條重鏈,我們可以之間線段樹維護區間乘積。
然後到了一條重鏈鏈頭,因為這個點是它父親的輕兒子,我們需要更新它父親節點所在的點的轉移矩陣。這樣子一直跳到根節點就可以了。
細節部分:
-
對於一個點,查詢其 \(dp\) 值,需要從這個點一直查到區間的鏈尾。
因此,樹剖時我們多維護一個 \(end[i]\) ( \(i\) 是一條重鏈的鏈頭) ,表示以 \(i\) 為鏈頭的這條鏈,鏈尾(葉子節點) 在 \(DFS\) 序上的位置。 -
更新線段樹上某個點的轉移矩陣時,傳入的如果是矩陣,遞迴下去常數太大。
解決方法:線上段樹外,維護一個矩陣組 \(val[i]\) , 表示每個點對應的轉移矩陣,這樣更新時,直接賦值進來即可。
程式碼:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=2e5+5,V=4e5+5,INF=0x7F7F7F7F;
struct matrix{
int mat[2][2];
matrix(){memset(mat,-0X3F,sizeof(mat));}
inline matrix operator * (matrix b){
matrix c;
for(int i=0;i<2;i++) for(int j=0;j<2;j++) for(int k=0;k<2;k++)
c.mat[i][j]=max(c.mat[i][j],mat[i][k]+b.mat[k][j]);
return c;
}
};
int n,m;
int head[N],nxt[M],ver[M],tot;
int A[N];
int fa[N],sizes[N],dep[N],dfn[N],top[N],id[N],son[N],End[N],cnt;
int dp[N][2];
matrix val[N];
struct SegTree{
int L[V],R[V]; matrix M[V];
void push_up(int x){M[x]=M[x<<1]*M[x<<1|1];}//更新就更新對應矩陣
void build(int l,int r,int x){
L[x]=l; R[x]=r;
if(l==r){
M[x]=val[dfn[L[x]]]; return;//賦值方式
}
int mid=l+r>>1;
build(l,mid,x<<1); build(mid+1,r,x<<1|1);
push_up(x);
}
void update(int pos,int x){
if(L[x]==R[x]){
M[x]=val[dfn[pos]];
return;
}
int mid=(L[x]+R[x])>>1;
if(pos<=mid) update(pos,x<<1);
else update(pos,x<<1|1);
push_up(x);
}
matrix query(int l,int r,int x){
if(L[x]==l&&R[x]==r) return M[x];
int mid=(L[x]+R[x])>>1;
if(r<=mid) return query(l,r,x<<1);
else if(l>mid) return query(l,r,x<<1|1);
else return query(l,mid,x<<1)*query(mid+1,r,x<<1|1);//dp即矩陣
}
}T;
void add(int x,int y){
ver[++tot]=y; nxt[tot]=head[x];head[x]=tot;
}
void dfs1(int x,int father){
sizes[x]=1;
dep[x]=dep[father]+1;
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==father) continue;
fa[y]=x;
dfs1(y,x);
sizes[x]+=sizes[y];
if(sizes[y]>sizes[son[x]]||!son[x]) son[x]=y;
}
}
void dfs2(int x,int topfather){
id[x]=++cnt; dfn[cnt]=x;
top[x]=topfather;
End[topfather]=max(End[topfather],cnt);//end陣列記錄鏈頭對應鏈尾位置
//更新g陣列,即更新val
dp[x][0]=0; dp[x][1]=A[x];
val[x].mat[0][0]=val[x].mat[0][1]=0;//記錄每個點的矩陣
val[x].mat[1][0]=A[x];
if(son[x]!=0){
dfs2(son[x],topfather);
dp[x][0]+=max(dp[son[x]][0],dp[son[x]][1]);
dp[x][1]+=dp[son[x]][0];
}
for(int i=head[x];i;i=nxt[i]){
int y=ver[i];
if(y==fa[x]||y==son[x]) continue;
dfs2(y,y);
dp[x][0]+=max(dp[y][0],dp[y][1]);
dp[x][1]+=dp[y][0];
val[x].mat[0][0]+=max(dp[y][0],dp[y][1]);//子兒子可選可不選的最大獨立集
val[x].mat[0][1]=val[x].mat[0][0];//同上
val[x].mat[1][0]+=dp[y][0];//子兒子不能選
}
}
void update_path(int x,int z){
val[x].mat[1][0]+=z-A[x];//因為這個地方還存的有子兒子不能選的值,因此不能直接賦值為 z,
A[x]=z;//更新對應點的值
matrix last,now;
while(x!=0){//不斷進行更新
last=T.query(id[top[x]],End[top[x]],1);//
T.update(id[x],1);//找到需要更改的地方,進行更新
now=T.query(id[top[x]],End[top[x]],1);
x=fa[top[x]];//一直找重鏈,直到頂端
val[x].mat[0][0]+=max(now.mat[0][0],now.mat[1][0])-max(last.mat[0][0],last.mat[1][0]);
//之前可選可不選-現在可選可不選
val[x].mat[0][1]=val[x].mat[0][0];
val[x].mat[1][0]+=now.mat[0][0]-last.mat[0][0];
//之前不選-現在不選
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&A[i]);
for(int i=1,x,y;i<n;i++){
scanf("%d%d",&x,&y); add(x,y); add(y,x);
}
dfs1(1,0);
dfs2(1,1);
T.build(1,n,1);
for(int i=1,x,y;i<=m;i++){
scanf("%d%d",&x,&y);
update_path(x,y);
matrix ans=T.query(id[1],End[1],1);
printf("%d\n",max(ans.mat[0][0],ans.mat[1][0]));
}
system("pause");
return 0;
}