知識點 - 點分治
解決問題類型:
樹上路徑問題的工具,舉個例子:
給定一棵樹和一個整數 kk ,求樹上邊數等於 kk 的路徑有多少條
實現
原理
如圖,假設我們選出一個根 ,那麼答案路徑肯定是要麼被一個子樹所包含,要麼就是跨過 ,在黑子樹中選擇一部分路徑,在紅子樹中選擇一部分路徑,然後從 處拼起來形成一條答案路徑
分類討論不會寫的,這輩子都不可能寫分類討論的
仔細想一下,發現情況1(被一個子樹包含)中,答案路徑上的一點變爲根 ,就成了情況2(在兩棵子樹中)
[外鏈圖片轉存失敗(img-ljAdxBxY-1568865920016)(https://a-failure.github.io/img/study/dianfenzhi6.png)]
如圖, 爲根的子樹中存在答案(藍色實邊路徑),可以看成以 爲根的兩棵子樹存在答案,所以只用處理情況2就行了,可以用分治的方法,這應該是點分治的基本原理
先從找好一個根開始
選根(選重心)
首先根不能隨便選,選根不同會影下面遍歷的效率的,如圖:
[外鏈圖片轉存失敗(img-DJGd2xth-1568865920018)(https://a-failure.github.io/img/study/dianfenzhi3.png)]
顯然選 爲根比選 爲根不優,選 最多遞歸2層,選 最多遞歸4層
顯然可以發現找樹的重心(重心所有的子樹的大小都不超過整個樹大小的一半)是最優的
我們可以根據每個點子樹大小確定根,當根的最大的子樹最小時肯定是重心
一個簡單的樹形 就能搞定
void GET_ROOT(int x,int fa)//x爲當前點,fa爲父親節點
{
f[x]=0,siz[x]=1;//f表示這個點最大子樹的大小,siz是這個點子樹大小的和
for(int i=h[x];i;i=c[i].x)//枚舉兒子
{
int y=c[i].y;
if(use[y]||y==fa) continue;//use表示之前遍歷過了,這裏沒啥用
GET_ROOT(y,x);//往下遍歷
f[x]=max(f[x],siz[y]);//更新f
siz[x]+=siz[y];
}
f[x]=max(f[x],Siz-siz[x]);//Siz表示在現在這棵子樹中點的總數,開始時Siz=n,除了枚舉的兒子所在的子樹外,還有一棵子樹是上面的那一堆,容斥原理
if(f[x]<f[rt]) rt=x;//更新root
}
因爲之後的分治過程還需要對子樹單獨找重心,所以代碼中有 ,但是開始對整棵樹無影響
求距離
找到根了,現在我們可以 一遍重心的子樹,求出重心到子樹各個點的距離
然後可以枚舉子樹裏的兩個點,如果兩個點到重心的距離和爲 (題目要找距離爲 的點對),那麼答案
這是第二種情況,第一種情況就讓距離根爲 的點跟重心配對就行了,因爲重心到重心的距離爲
如何統計答案呢?
統計答案
肯定不能直接枚舉啊… 的複雜度啊喂
考慮枚舉一個點,另一個點可以通過二分來求解, 一下讓距離有序,這樣要找距離爲 -枚舉點的距離的點的個數,因爲相同距離的點現在是連續的,所以可以二分出左右邊界
int look(int l,int x)//找左邊界
{
int ans=0,r=cnt;
while(l<=r)
{
int mid(l+r>>1);
if(d[mid]<x) l=mid+1;
else ans=mid,r=mid-1;
}
return ans;
}
int look2(int l,int x)//找右邊界
{
int ans=0,r=cnt;
while(l<=r)
{
int mid(l+r>>1);
if(d[mid]<=x) ans=mid,l=mid+1;
else r=mid-1;
}
return ans;
}
void GET_NUM(int x,int fa,int D)//求重心到子樹每個點的距離
{
for(int i=h[x];i;i=c[i].x)
{
int y=c[i].y;
if(use[y]||y==fa) continue;
d[++cnt]=D+c[i].dis;
GET_NUM(y,x,d[cnt]);
}
}
int GET_ANS(int x)//求答案
{
d[cnt=1]=0;
GET_NUM(x,0,0);
sort(d+1,d+1+cnt);//排下序
int l=1,ans=0;
while(l<cnt&&d[l]+d[cnt]<k) ++l;
while(l<cnt&&k-d[l]>=d[l])
{
int D1(look(l+1,k-d[l])),D2(look2(l+1,k-d[l]));//枚舉一個點,因爲和是k,所以可以直接求出另一點到重心的距離,二分一下就行了
if(D2>=D1) ans+=D2-D1+1;
++l;
}
return ans;
}
void dfs(int x)//分治函數
{
use[x]=1,ans+=GET_ANS(x);//統計這個點的答案
for(int i=h[x];i;i=c[i].x)
{
int y=c[i].y;
if(use[y]) continue;//防止找父親
Siz=siz[y],rt=0;//下一個重心肯定在當前點的子樹裏
GET_ROOT(y,x),dfs(rt);//找下一個重心
}
}
也可以通過移動兩個指針來實現只要不是枚舉兩個點就行了
這樣我們就快樂的A掉了這道題
[外鏈圖片轉存失敗(img-WBpnyRw6-1568865920019)(https://a-failure.github.io/img/qaq/happy.jpg)]
了嗎?
求一遍發現答案不對誒…似乎多了幾種情況?如圖
: [外鏈圖片轉存失敗(img-0Vkwnq7F-1568865920019)(https://a-failure.github.io/img/study/dianfenzhi4.png)]
假設 ,圖中 到 的距離爲, 到 的距離爲,合起來是,這時候答案,但是顯然這兩個點最短路徑不是
這是因爲ta們在同一子樹中,到重心的路徑有重疊部分
統計答案 二連擊
處理方法:
1.可以求距離的時候把點染色,不同子樹不同顏色,那麼求答案的時候就得枚舉每個符合答案的每個點看是否不在一個子樹裏
2.可以求當前點兒子的答案,統計兒子答案時各個點的距離加上兒子到根的距離,即把符合在一個子樹條件的情況統計出來,最後這個點的答案減去兒子答案就行了
[外鏈圖片轉存失敗(img-lI5YJNgM-1568865920020)(https://a-failure.github.io/img/study/dianfenzhi5.png)]
圖中求 兒子 的答案,因爲加上兒子到重心的距離,所以 的距離還是, 的距離還是,這樣就把不符合條件的答案去掉了
int GET_ANS(int x,int D)//改一下這裏就行了
{
d[cnt=1]=D;
GET_NUM(x,0,D);
sort(d+1,d+1+cnt);
int l=1,ans=0;
while(l<cnt&&d[l]+d[cnt]<k) ++l;
while(l<cnt&&k-d[l]>=d[l])
{
int D1(look(l+1,k-d[l])),D2(look2(l+1,k-d[l]));
if(D2>=D1) ans+=D2-D1+1;
++l;
}
return ans;
}
void dfs(int x)//分治函數也改一下
{
use[x]=1,ans+=GET_ANS(x,0);//當前點答案
for(int i=h[x];i;i=c[i].x)
{
int y=c[i].y;
if(use[y]) continue;//防止找父親
ans-=GET_ANS(y,1);//去掉滿足在一個子樹條件的不合法答案
Siz=siz[y],rt=0;
GET_ROOT(y,x),dfs(rt);//找下一個重心
}
}
複雜度
每次處理找樹的重心,保證遞歸層數不超過 ,求距離複雜度是 ,這裏處理答案是 ,所以這個題總複雜度是 的…?反正過幾萬的數據是綽綽有餘的(逃
補充:經 Aufun 大爺嘲諷教導後,因爲有的題可以用桶排序,所以複雜度可以降到
當然有的題桶開不下必須 了QAQ我一直用sort竟然沒被卡
複雜度:
操作時間複雜度爲
例題
題目描述
給定一棵有n個點的樹
詢問樹上距離爲k的點對是否存在。
代碼
// luogu-judger-enable-o2
#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
#define rep(i,j,k) for(int i = (int)j;i <= (int)k;i ++)
#define debug(x) cerr<<#x<<":"<<x<<endl
#define pb push_back
typedef long long ll;
const int MAXN = (int)1e5+7;
const int INF = (int)0x3f3f3f3f;
inline int read() { int c = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar();}
while (ch >= '0' && ch <= '9') {c = c * 10 + ch - '0';ch = getchar();}
return c * f;
}
struct Node {
int v,w;
Node(int v = 0,int w = 0):v(v),w(w){}
};
vector<Node> G[MAXN];
vector<int> dis;
bool vis[MAXN];
int N,M,K,root,sz[MAXN],maxv[MAXN],mx,num[10000007];
ll ans;
bool flag = 0;
inline void dfs_sz(int u,int fa) {
maxv[u] = 0;sz[u] = 1;
rep(i,0,G[u].size()-1) {
int v = G[u][i].v;
if (v == fa || vis[v]) continue;
dfs_sz(v,u);
sz[u] += sz[v];
maxv[u] = max(maxv[u],sz[v]);
}
}
inline void dfs_root(int u,int fa,int rt) {
maxv[u] = max(maxv[u],sz[rt]-sz[u]);
if (mx > maxv[u]) {
root = u;
mx = maxv[u];
}else return;
rep(i,0,G[u].size()-1) {
int v = G[u][i].v;
if (v == fa|| vis[v]) continue;
dfs_root(v,u,rt);
}
}
inline void dfs_dis(int u,int fa,int weight) {
//if (weight > K) return;
dis.pb(weight);
rep(i,0,G[u].size()-1) {
int v = G[u][i].v;
if (vis[v] || fa == v) continue;
dfs_dis(v,u,weight+G[u][i].w);
}
}
inline void cal_dis(int u,int weight) {
dis.clear();
dfs_dis(u,-1,weight);
int l = 0,r = dis.size()-1;
rep(i,0,r) {
rep(j,i+1,r) {
if (!weight) {
num[dis[i] + dis[j]] ++;
}
else {
num[dis[i] + dis[j]] --;
}
}
}
}
inline void DFS(int u) {
mx = N;
dfs_sz(u,-1);
dfs_root(u,-1,u);
int rt = root; //因爲在後續的遞歸中root的值會變化,所以這裏必須使用臨時變量儲存起來
vis[rt] = 1;
cal_dis(rt,0);
rep(i,0,G[rt].size()-1) {
int v = G[rt][i].v;
if (vis[v]) continue;
cal_dis(v,G[rt][i].w);
DFS(v);
}
}
void init(int n) {
rep(i,0,n) G[i].clear();
memset(vis,0,sizeof(bool)*(n+1));
ans = 0;
}
int main()
{
scanf("%d %d",&N,&M);
init(N);
rep(i,1,N-1) {
int u,v,w;
u = read(),v = read(),w = read();
G[u].pb(Node(v,w));
G[v].pb(Node(u,w));
}
DFS(1);
rep (i,1,M) {
scanf("%d",&K);
if (num[K]) puts("AYE");
else puts("NAY");
}
}
/*
5 4
1 2 1
1 3 2
1 4 5
4 5 10
2 1
1 2 1
*/