c++分治法求最大最小值实现_点分治入门

缘起

楼教主 男人八题之一   Poj 1741 Tree

分析

一棵n个顶点的树, 每条边有一个长度([1,1000]中的正整数), 定义dis(u, v)是u和v之间的树上距离. 给定
整数k, 每个对(u, v)称之为合法如果dis(u, v)<=k,写一个程序计算树上有多少合法对?

【输入】
多样例. 每个样例开始于n,k, n<=1w, 然后n-1行每行包含u,v,l 表示u和v之间有边长为l
输入以0,0 结束

【输出】
答案

【样例输入】
5 4
1 2 3
1 3 1
1 4 2
3 5 1
0 0

【样例输出】
8

【限制】
1s

本题是点分治的板题~

其实点分治是树分治的一种, 树分治包括点分治和边分治. 边分治不在本文介绍范围内.

点分治是大规模处理树上路径的一种算法. 分治思想大家肯定都是十分清楚的.

2f3a94cd63b85a3e9b507903d82e7717.png
image

就像要处理上图的树问题,我们可以将u子树的问题分治为A、B、C、D、V 这五棵子树的子问题,得到答案后再合并.  这就是点分治(唔,貌似说了跟没说一样,dfs它喵的不就是这样吗? 还特地发明个点分治?)

当然不是咯~ 点分治的核心思想并不是在分治,而是在树根u的选择上,确切讲,点分治一般处理无根树(也就是我们可以随意定根),选取根u之后要使得转成的有根树的最多节点的子树的节点最少。

u它喵的不就是树的重心吗?

树的重心在本公众号中有介绍过,参见【1】CodeForces 686D(2019 ICPC 徐州现场赛M)

这里说一下为什么分治要选取树的重心? 其实是显而易见的,如果树退化成链表的话,你傻傻的从链的一端不断地递归的话,复杂度显然是O(n^2)啊~ 那还分治个pi啊,直接打暴力好了. 显然应该选取链的中点作为分治点. 即

8791bc99fa6e2394db29d3558c915a80.png
image

**树的重心有一个很重要的性质. **

每一棵子树的大小都不超过n/2, esp. 重儿子的大小不会超过n/2

证明用反证法,图1其中点u是重心,son[u]表示u点的最大的子树的大小,v是点u的重儿子,且size[v]>size[u]/2,因为size[v]>size[u]/2,所以u的其他子树加上点u的节点数小于size[u]/2,那么不难发现,我们选择点v作为重心,son[v]=size[v]−1

于是每一次找到重心,递归的子树大小是不超过原树大小的一半的,那么递归层数不会超过O(logn)层,每层需要处理O(n)规模的问题,所以点分治的时间复杂度为O(nlogn). 很完美的算法~

这里用本题讲一下点分治的板子。

首先,取树的重心为树根,将无根树转换为有根树(假设是图1)并且假设我们已经处理好了A、B、C、D、V 这些子树的子问题. 然后现在要考虑经过U点的路径(则路径的起点终点势必在不同的子树中)中<=k的条数.

怎么求呢?

选定重心u作为根之后,只需要dfs一次将u子树中所有节点到u的距离dis求出来, 然后求出所有满足dis[a]+dis[b]<=k的(a,b)的点对个数X即可(ps: 这个X的求解很简单,就是将dis排个序, 然后用双指针即可,具体见代码,O(n) 很快)

等等,不对啊~ 你这样求出的(a,b)有可能a--b并不经过u啊~ 这其实好办,只需要减去子树(例如A子树)中到u的距离和<=k的点对个数即可. 例如A子树中存在两个点(p,q), p--u--q的距离之和<=k,辣么就要将X减去这样的(p,q)点对个数.  即下面代码的114行做的事情

//#include "stdafx.h"
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include
#include
#include
using namespace std;
//#define LOCAL
const int maxn = 1e4+5, inf = 0x3f3f3f3f;
int n,k, head[maxn], cnt, dis[maxn], zx, mb, ans, tot, sz[maxn]; // zx是重心
bool v[maxn]; // v[i]=j表明i节点已经作为树根了
struct Arc
{
int from, to, nxt, len;
} g[maxn << 1];

void addarc(int from, int to, int len) {
g[cnt].from = from, g[cnt].to = to, g[cnt].len = len, g[cnt].nxt = head[from];
head[from] = cnt++;
}

void mmax(int &a, int b) {
if (a < b)
{
a = b;
}
}

void dfs2(int cur, int fa) // 得到各个节点的子树大小{
sz[cur] = 1;
for (int h = head[cur], to; ~h; h = g[h].nxt)
{
to = g[h].to;
if (to ^ fa && !v[to])
{
dfs2(to, cur);
sz[cur] += sz[to];
}
}
}

void dfs1(int cur, int fa, int nn) // 求树的重心, 返回cur子树大小. nn是求重心的树的总的节点个数{
int ret = 1, b = 0;
for (int h = head[cur], to; ~h; h = g[h].nxt)
{
to = g[h].to;
if (to ^ fa && !v[to])
{
dfs1(to, cur, nn);
ret += sz[to];
mmax(b, sz[to]);
}
}
mmax(b, nn - ret);
if (b < mb)
{
mb = b;
zx = cur;
}
}

void dfs3(int cur, int d, int fa) {
dis[tot++] = d;
for (int h = head[cur], to, len; ~h; h = g[h].nxt)
{
to = g[h].to;
len = g[h].len;
if (to ^ fa && !v[to])
{
dfs3(to, d + len, cur);
}
}
}

int kk(int cur, int d, int fa) // 返回cur子树中所有(a, b)的点对数, 其中(a,b)点对满足 (dis[a]+d)+(dis[b]+d)<=k, 注意,(a,b)和(b,a)视作相同点对哈,不能重复计数~ 其中dis[a]是a到cur的距离{
tot = 0;
dfs3(cur, d, fa); // 获取cur子树中所有节点a的dis[a]+d的值,存入dis[0, tot)数组中
sort(dis, dis + tot);
int ret = 0, i = 0, j = tot - 1; // 求dis[0,..,tot) 中满足dis[a]+dis[b] <=k的(a,b)对的数量,i,j是双指针
while(i < j)
{
if (dis[i] + dis[j] <= k)
{
ret += j - i; // 则 dis[i]和dis[i+1,...,j]都满足
++i;
}
else
{
--j;
}
}
return ret;
}

void dfs(int cur, int fa) // 点分治{
mb = inf; // 用于求重心的
dfs2(cur, fa); // 确定cur子树的大小sz[cur], 注意, 因为重心(即根节点)在不断变化中, 所以每次都要重新处理出sz来
dfs1(cur, fa, sz[cur]); // 求重心zx, 它将是新的树根
v[zx] = true;
int t = zx; // zx是全局变量, 随着递归会变, 故而用t缓存,下面都用t
ans += kk(t, 0, 0); // 这里传入的fa是0, 并不能阻止朝着zx去递归, 所以必须在dfs3方法中加入 !v[to]的判断(见72行)
for (int h = head[t], to, len; ~h; h = g[h].nxt) // 注意, 树根从cur变成了t=zx
{
to = g[h].to;
len = g[h].len;
if (!v[to]) // 防止回溯到t=zx去(因为v[zx]刚刚被赋予true)
{
ans -= kk(to, len, t);
dfs(to, t);
}
}
}

signed main() {
#ifdef LOCAL
freopen("d:\\data.in","r",stdin);
// freopen("d:\\my.out", "w", stdout);
#endif
while(scanf("%d%d", &n, &k), n)
{
memset(head, -1, sizeof(head));
memset(v, 0, sizeof(v));
cnt = ans = 0;
for (int i = 1, u, v, l; i < n; i++)
{
scanf("%d%d%d", &u, &v, &l);
addarc(u, v, l);
addarc(v, u, l);
}
dfs(1, 0);
printf("%d\n", ans);
}
return 0;
}

ac情况

StatusAccepted
Time219ms
Memory1108kB
Length2781
LangG++
Submitted2020-03-01 20:40:14
Shared
RemoteRunId21422332

这里不得不总结一下上面点分治的代码实现细节, 因为点分治的确细节比dsu on tree、平衡树启发式合并等算法显得啰嗦了一些——虽然思路十分的直观和简明,但是细节比较多.

  1. 所有的if(to ^ fa) 变成了 if(to ^ fa && !v[to]) 为什么?  举个例子251f0a99865e69cc4d08de4257fd0e98.png

    你从重心zx=9997递归进入9998、9999、10000组成的子树, 自然这棵子树的重心是9999, 然后从9999进入该子树,然后走进其孩子cur=9998子树的时候,如果判断条件仅仅是 if(to ^ fa) 的话, 注意,fa是9999,则to可以走到9997去. 但是显然,9998已经是叶子了, 不能再有任何子节点了. 所以!v[9997]的目的是让9998进入之后不能走到9997去.

  2. 103行,见注释

  3. 106行, 见注释

  4. 44行的求树的重心的板子要传入nn, 即要求重心的树的节点总数. 这是因为求重心要发生多次,所以求重心的树的规模也在不断变化(确切讲规模是在不断的减半,这是上面的树的重心的性质保证的),所以要传入.

最后讲两个点分治容易T的点

  1. 一旦被T,先打印一下你每次求出的重心(例如上面代码的102行之前). 就可以知道你求重心的代码是否写错.
  2. 一般不要在递归中写memset,容易被T.

参考

【1】CodeForces 686D(2019 ICPC 徐州现场赛M)

17d3e82810871bad8bd43613025e09b9.gif

温馨提示

如果你喜欢本文,请分享到朋友圈,想要获得更多信息,请关注ACM算法日常

ea4d023960d1ee4bc51d7afb3f023095.png

点赞的时候,请宠溺一点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值