【前言】
最近一段时间变成了通过题目学习算法,似乎整个人都乱套了(反思ing)
不过还好,现在又调整为了学算法后做题。(唉,最近一段时间有点急躁,要记住万事不能速成啊)
【正题】点分治
一句话:点分治主要用于树上路径点权统计问题。
一、【具体流程】
1,选取一个点,将无根树变成有根树
为了使每次的处理最优,我们通常要选取树的重心。
何为“重心”,就是要保证与此点连接的子树的节点数最大值最小,可以防止被卡。
重心求法:
1。dfs一次,算出以每个点为根的子树大小。
2。记录以每个节点为根的最大子树大小
3。判断:如果以当前节点为根更优,就更新当前根。
void getroot(int v,int fa)
{
son[v] = 1; f[v] = 0;//f记录以v为根的最大子树的大小
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
getroot(e[i].to,v);//递归更新
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);//比较每个子树
}
f[v] = max(f[v],sum-son[v]);//别忘了以v父节点为根的子树
if(f[v] < f[root]) root = v;//更新当前根
}
2、处理连通块中通过根节点的路径。
(注意,是通过根节点的路径,所以后面要去掉同一子树内部的路径,即去重)
3、标记根节点(相当于处理后,将根节点从子树中删除)。
4、递归处理当前点为根的每棵子树。
int solve(int v)
{
vis[v] = 1;//标记
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
root = 0;
sum = son[e[i].to];
getroot(e[i].to,v);
solve(root);//递归处理下一个连通块
}
}
int main()
{
sum = f[0] = n;//初始化
root = 0;
getroot(1,0);//找重心
solve(root);//点分治
}
【注释】:作者是用 son[] 来表示节点x为根的子树大小,可能他人更多地是用size[]来表示,二者同意。
二、【POJ 1741 & BZOJ 1468 & BZOJ 3365】
给你一棵TREE,以及这棵树上边的距离.问有多少对点它们两者间的距离小于等于K。
【题解】:
我们找到树的重心,然后dfs,求出每个点到root的距离deep,然后对deep排序,扫描哪些点对是符合的。
但是,点分治要求处理的路径是经过root,所以如果一条路径是在同一个子树之内的就不符合要求,所以还要对子树dfs一下,然后去重。
接下来处理好root后,就可以处理其他连通块了,即递归其子树。
【代码】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 10010
#define inf 1e9+10
struct node{int to,c,next;}g[N*2];
int head[N],m;
int son[N],f[N];
bool vis[N];
int d[N],deep[N];
int n,sum,root,k,ans;
void add_edge(int from,int to,int cost)
{
g[++m].next = head[from];
head[from] = m;
g[m].to = to; g[m].c = cost;
}
void getroot(int v,int fa)
{
son[v] = 1; f[v] = 0;
for(int i = head[v];i;i=g[i].next)
if(g[i].to != fa && !vis[g[i].to])
{
getroot(g[i].to,v);
son[v] += son[g[i].to];
f[v] = max(f[v],son[g[i].to]);
}
f[v] = max(f[v],sum - son[v]);
if(f[v] < f[root]) root = v;
}
void getdeep(int v,int fa)
{
deep[++deep[0]] = d[v];
for(int i = head[v];i;i=g[i].next)
if(g[i].to != fa && !vis[g[i].to])
{
d[g[i].to] = d[v] + g[i].c;
getdeep(g[i].to,v);
}
}
int cal(int v,int cost)
{
d[v] = cost; deep[0] = 0;
getdeep(v,0);
sort(deep+1,deep+deep[0]+1);
int l = 1,r = deep[0],sum = 0;
while(l < r) {
if(deep[l]+deep[r] <= k) {
sum += r-l;
l++;
} else r--;
}
return sum;
}
void solve(int v)
{
ans += cal(v,0);
vis[v] = 1;
for(int i = head[v];i;i=g[i].next)
if(!vis[g[i].to])
{
ans -= cal(g[i].to,g[i].c);
sum = son[g[i].to];
root = 0;
getroot(g[i].to,0);
solve(root);
}
}
int main()
{
int u,v,w;
while(scanf("%d%d",&n,&k) && n && k)
{
ans = root = m = 0;
memset(vis,0,sizeof(vis));
memset(head,0,sizeof(head));
for(int i = 1;i < n;i++)
{
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
add_edge(v,u,w);
}
f[0] = inf;
sum = n;
getroot(1,0);
solve(root);
printf("%d\n",ans);
}
return 0;
}
【补】:若是距离等于k,cal可以改成:
int cal(int v,int cost)
{
d[v] = cost; deep[0] = 0;
getdeep(v,0);
sort(deep+1,deep+deep[0]+1);
int r = deep[0],res = 0;
for(int l = 1;l < r;l++)
while(deep[l]+deep[r] >= k) {
if(deep[l] + deep[r] == k) res++;
r--;
}
return res;
}
三、【BZOJ 2152】
由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
【题解】:
感觉这道更好处理,不用快排,也不用去重。我们对于当前的树,直接找到重心V,然后从V出发,搜索与V相邻的点,计算边长的余数分别是是0,1,2的情况数,用t[0],t[1],t[2]分别表示。
显然答案就是 t[1]*t[2]*2+t[0]*t[0]。
【代码】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 20010
struct node{int to,c,next;}e[N*2];
int head[N],m;
int ans,root,t[4],d[N],son[N],f[N],sum;
bool vis[N];
void add_edge(int from,int to,int cost)
{
e[++m].next = head[from];
head[from] = m;
e[m].to = to; e[m].c = cost;
}
void getroot(int v,int fa)
{
son[v] = 1;f[v] = 0;
for(int i = head[v];i;i = e[i].next)
if(!vis[e[i].to] && e[i].to != fa)
{
getroot(e[i].to,v);
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);
}
f[v] = max(f[v],sum-son[v]);
if(f[v] < f[root]) root = v;
}
void getdeep(int v,int fa)
{
t[d[v]]++;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to] && e[i].to != fa)
{
d[e[i].to] = (d[v] + e[i].c)%3;
getdeep(e[i].to,v);
}
}
int cal(int v,int w)
{
t[0] = t[1] = t[2] = 0;
d[v] = w;
getdeep(v,0);
return t[1]*t[2]*2+t[0]*t[0];
}
void solve(int v)
{
ans += cal(v,0); vis[v] = 1;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to])
{
ans -= cal(e[i].to,e[i].c);
root = 0; sum = son[e[i].to];
getroot(e[i].to,0);
solve(root);
}
}
inline int gcd(int a,int b){return b == 0 ? a : gcd(b,a%b);}
int main()
{
int n,u,v,w;
scanf("%d",&n);
for(int i = 1;i < n;i++)
{
scanf("%d%d%d",&u,&v,&w);
w %= 3;
add_edge(u,v,w); add_edge(v,u,w);
}
sum = n;f[0] = n;
root = ans = 0;
getroot(1,0);
solve(root);
int x = gcd(ans,n*n);
printf("%d/%d\n",ans/x,n*n/x);
return 0;
}
先到这里,我下去逛逛。未完待续……
好了,我又回来了
四、【BZOJ 2599】
给一棵树,每条边有权.求一条简单路径,权值和等于K,且边的数量最小.N <= 200000, K <= 1000000
【题解】:
参考黄学长的题解啊。
开一个100W的数组t,t[i]表示权值为i的路径最少边数
找到重心分成若干子树后, 得出一棵子树的所有点到根的权值和x,到根a条边,用t[k-x]+a更新答案,全部查询完后,再用所有a更新t[x],这样可以保证不出现点分治中的不合法情况。
把一棵树的所有子树搞完后再遍历所有子树恢复T数组,如果用memset应该会比较慢
看的稀里糊涂的,但还是好像懂了一点啊。
d数组 表示已经有几条边
dis数组 表示子树中的点到根的距离
add函数用于更新和初始化(好像有这个功能吧)
【代码】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 200010
#define inf 1000000000
struct node{int to,c,next;}e[N*2];
int head[N],m,k;
bool vis[N];
int t[1000010];
int sum,f[N],dis[N],d[N],son[N],root,ans;
void add_edge(int from,int to,int cost)
{
e[++m].next = head[from];
head[from] = m;
e[m].to = to;e[m].c = cost;
}
void getroot(int v,int fa)
{
son[v] = 1;f[v] = 0;
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
getroot(e[i].to,v);
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);
}
f[v] = max(f[v],sum-son[v]);
if(f[v] < f[root]) root = v;
}
void cal(int v,int fa)
{
if(dis[v] <= k) ans = min(ans,d[v]+t[k-dis[v]]);
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
d[e[i].to] = d[v] + 1;
dis[e[i].to] = dis[v] + e[i].c;
cal(e[i].to,v);
}
}
void add(int v,int fa,bool flag)
{
if(dis[v] <= k) {
if(flag) t[dis[v]] = min(t[dis[v]],d[v]);
else t[dis[v]] = inf;
}
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to])
add(e[i].to,v,flag);
}
void solve(int v)
{
vis[v] = 1;t[0] = 0;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
d[e[i].to] = 1;
dis[e[i].to] = e[i].c;
cal(e[i].to,0);
add(e[i].to,0,1);
}
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) add(e[i].to,0,0);
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
root = 0;
sum = son[e[i].to];
getroot(e[i].to,0);
solve(root);
}
}
int main()
{
int n,u,v,w;;
scanf("%d%d",&n,&k);
for(int i = 1;i <= k;i++) t[i] = n;
for(int i = 1;i < n;i++) {
scanf("%d%d%d",&u,&v,&w);
u++; v++;
add_edge(u,v,w); add_edge(v,u,w);
}
ans = sum = f[0] = n;
root = 0;
getroot(1,0);
solve(root);
if(ans != n) printf("%d\n",ans);
else puts("-1");
return 0;
}
啊!
今天对点分治的学习差不多就到这里了。
笔记结束,开始刷水题玩喽。
补:
五、【BZOJ 1316】
一棵n个点的带权有根树,有p个询问,每次询问树中是否存在一条长度为Len的路径,如果是,输出Yes否输出No.
【题解】:
运用点分治统计点到重心的距离,再两次二分查找距离,判断有多少条路径长度为k(这样为了方便去重)。
听说点分治的常数比较大,所以将所有询问在一次点分治中一起做。
【代码】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 10010
struct node{int to,w,next;}e[N*2];
int head[N],m,p;
int q[110],f[N],son[N],sum,root,d[N],deep[N];
bool vis[N];
void add_edge(int from,int to,int cost)
{
e[++m].next = head[from];
head[from] =m;
e[m].w = cost; e[m].to = to;
}
void getroot(int v,int fa)
{
son[v] = 1;f[v] = 0;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to] && e[i].to != fa) {
getroot(e[i].to,v);
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);
}
f[v] = max(f[v],sum-son[v]);
if(f[v] < f[root]) root = v;
}
void getdeep(int v,int fa)
{
deep[++deep[0]] = d[v];
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
d[e[i].to] = d[v] + e[i].w;
getdeep(e[i].to,v);
}
}
int findl(int L,int R,int k)
{
int ans = 0;
while(L <= R){
int mid = (L+R)>>1;
if(deep[mid] == k){ans = mid;R = mid-1;}
else if(deep[mid] < k) L = mid + 1;
else R = mid - 1;
}
return ans;
}
int findr(int L,int R,int k)
{
int ans = -1;
while(L <= R) {
int mid = (L+R)>>1;
if(deep[mid] == k){ans = mid;L = mid+1;}
else if(deep[mid] < k) L = mid+1;
else R = mid - 1;
}
return ans;
}
int cal(int v,int now,int k)
{
d[v] = now; deep[0] = 0;
getdeep(v,0);
sort(deep+1,deep+deep[0]+1);
int t = 0;
for(int i = 1;i <= deep[0];i++) {
if(deep[i] + deep[i] > k) break;
int l = findl(i,deep[0],k-deep[i]);
int r = findr(i,deep[0],k-deep[i]);
t += r-l+1;
}
return t;
}
int ans[110];
void solve(int v)
{
for(int i = 1;i <= p;i++) ans[i] += cal(v,0,q[i]);
vis[v] = 1;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
for(int j = 1;j <= p;j++)
ans[j] -= cal(e[i].to,e[i].w,q[j]);
sum = son[e[i].to];
root = 0;
getroot(e[i].to,0);
solve(root);
}
}
int main()
{
int n,u,v,w;
scanf("%d%d",&n,&p);
m = 0;
for(int i = 1;i < n;i++)
{
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w); add_edge(v,u,w);
}
for(int i = 1;i <= p;i++) scanf("%d",&q[i]);
sum = f[0] = n;
root = 0;
getroot(1,0);
solve(root);
for(int i = 1;i <= p;i++)
if(ans[i]) puts("Yes"); else puts("No");
return 0;
}
吾 点分治 之道路大概结束于此。
PS:4月3日,第四次更新。


4028

被折叠的 条评论
为什么被折叠?



