• 点分治学习笔记


    简介

    点分治顾名思义是利用分治的思想,将原本要(n^3)的操作降到(Theta (n^2 imes log_n)),大大提高了优越性(不提高效率谁学啊喂)

    基本思路

    点分治如同普通的分治一样,普通的分治是处理两种情况:在一个区间的任务转化为两个区间的任务,把在两个子区间的任务在两个子区间里运算,而在跨两个子区间的任务则在本次处理掉,可以提高效率。推广到树上,对于要找到的路径(注意不是边),路径可以不经过当前的根节点,也可以经过当前的根节点,把不经过当前根节点的在自己的子树中去处理,而把经过当前根节点的在此时处理掉,因为树的深度限制要想达到提高效率,就要要找到一个节点,使得自己的子树大小均衡,这个节点我们称之为树的重心。通过不断的找树的重心来保证时间效率的优秀性,要是不找重心,树变成一条链,会把点分治(这东西现在还能叫分治?)卡成暴力的复杂度。

    例题 Tree

    给定一棵树,树上的边有权值,给定一个阈值k,请统计这棵树上总长度小于等于k的路径个数。路径长度为路径路径上所有边的权值和。

    框架

    这是点分治中很经典的题目,首先写一下点分治的整体代码:

    void DFS (int u) {
    	ans += Calc(u, 0);//求经过u节点的所有方案数
    	vis[u] = true;//表示访问过
    	for (register int i = head[u]; i; i = edge[i].next) {
    		int v = edge[i].to;
    		if (vis[v]) continue;
    		ans -= Calc (v, edge[i].val);//由于同一棵子树中可能有两个点距离满足条件,需要去重
    		root = 0;//重新找重心
          		Tsiz = siz[v];//表示当前子树的大小
    		getroot(v, u);//找重心
    		DFS(root);//分治,处理每个子树中的问题
    	}
    }
    

    现在看不懂没关系,懂了大体结构再往下读。

    找树根

    void getroot (int u, int fa) {
    	siz[u] = 1, w[u] = 0;//预处理
    	for (register int i = head[u]; i; i = edge[i].next) {
    		int v = edge[i].to;
    		if (v == fa || vis[v]) continue;
    		getroot (v, u);//递归求重链和树的大小
    		siz[u] += siz[v];
    		w[u] = max (w[u], siz[v]);//求当前节点子树中节点最多的
    	}
    	w[u] = max(w[u], Tsiz - siz[u]);//该根节点还有其他的子树
          	if (w[u] < w[root]) root = u;
    }
    

    w数组指的是该根节点下的所有子树中size最大的值,而要是一一个节点作为跟节点,那么该树中其他的点都是该根节点的子树,找到一个点让每个子树都很平均即找到了树根。

    求方案数

    找到了一个树根之后,以这个跟节点作为起点,有dfs求出该子树中的方案数,两个点到根节点的距离之和小于k即满足要求,同时如果两个节点在该根节点的同一棵子树中,就会出现不合法的方案,然后我们只需要在每个子树中减去多余的方案数即可。

    void dfs (int u, int fa, int dis) {
    	a[++cnt] = dis;//将每个节点的距离都推进数组a
    	for (register int i = head[u]; i; i = edge[i].next) {
    		int v = edge[i].to;
    		if (v == fa || vis[v]) continue;
    		dfs (v, u, dis + edge[i].val);//递归处理
    	}
    }
    int Calc (int u, int dis) {
    	cnt = 0;
    	dfs (u, 0, dis);//预处理子树中的方案数
    	sort (a + 1, a + 1 + cnt);//排序后可以发现能用双指针维护,要是不用双指针效率大大降低。。
    	int sum = 0;
    	for (register int i = 1, j = cnt; ; i++) {
    		while (j && a[i] + a[j] > k) j--;//找到合法的区间
    		if (j < i) break;//防止同一个方案计算两次
    		sum += j - i + 1;
    	}
    	return sum;
    }
    

    在返回来看主分治的代码中,

    void DFS (int u) {
          ans += Calc (u, 0);
          for (register int i = head[u]; i; i = edge[i].next) {
                ans -= Calc (v, edge[i].val);
          }
    }
    

    注意在找子树中不合法方案数的时候一定不要找成子树中的方案,在Calc传参的时候要加上该子树跟节点到u的距离,这样找到的所有方案数才是真正多加上的方案数,否则会减多了。可以自己手画一下图来理解一下。

    总结

    学会点分治只是一种思想,还需要自己多加练习,才能很好的利用点分治解决一些题目。
    放一下整体的代码

    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #include <queue>
    #include <iostream>
    using namespace std;
    const int maxn = 4e4 + 50;
    inline int read () {
    	int x = 0, f = 1; char ch = getchar();
    	for (;!isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
    	for (; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
    	return x * f;
    }
    int n, k;
    int ans;
    struct Edge {
    	int to, next, val;
    } edge[maxn << 1];
    int cnt;
    int tot, head[maxn];
    int a[maxn];
    int root;
    int Tsiz;
    void addedge (int a, int b, int c) {
    	edge[++tot].next = head[a];
    	head[a] = tot;
    	edge[tot].to = b;
    	edge[tot].val = c;
    }
    bool vis[maxn];
    int siz[maxn], w[maxn];
    void getroot (int u, int fa) {
    	siz[u] = 1, w[u] = 0;
    	for (register int i = head[u]; i; i = edge[i].next) {
    		int v = edge[i].to;
    		if (v == fa || vis[v]) continue;
    		getroot (v, u);
    		siz[u] += siz[v];
    		w[u] = max (w[u], siz[v]);
    	}
    	w[u] = max(w[u], Tsiz - siz[u]);
    	if (w[u] < w[root]) root = u;
    }
    void dfs (int u, int fa, int dis) {
    	a[++cnt] = dis;
    	for (register int i = head[u]; i; i = edge[i].next) {
    		int v = edge[i].to;
    		if (v == fa || vis[v]) continue;
    		dfs (v, u, dis + edge[i].val);
    	}
    }
    int Calc (int u, int dis) {
    	cnt = 0;
    	dfs (u, 0, dis);
    	sort (a + 1, a + 1 + cnt);
    	int sum = 0;
    	for (register int i = 1, j = cnt; ; i++) {
    		while (j && a[i] + a[j] > k) j--;
    		if (j < i) break;
    		sum += j - i + 1;
    	}
    	return sum;
    }
    void DFS (int u) {
    	ans += Calc(u, 0);
    	vis[u] = true;
    	for (register int i = head[u]; i; i = edge[i].next) {
    		int v = edge[i].to;
    		if (vis[v]) continue;
    		ans -= Calc (v, edge[i].val);
    		root = 0;
    		Tsiz = siz[v];
    		getroot(v, u);
    		DFS(root);
    	}
    }
    int main () {
    	n = read();
    	int x, y, z;
    	for (register int i = 1; i <= n - 1; i ++) {
    		x = read(), y = read(), z = read();
    		addedge (x, y, z), addedge (y, x, z);
    	}
    	cin >> k;
    	w[0] = 0x3f3f3f3f;
    	root = 0;
    	getroot (1, 0);
    	Tsiz = n;
    	DFS (root);
    	printf ("%d
    ", ans - n);//注意由于在每次dfs维护dis的时候把跟节点也加入到了a数组中,每个点也多算了一次,所以要减去n才是正确答案。
    	return 0;
    }
    

    完结散花qwq

  • 相关阅读:
    HDU 3999 The order of a Tree (排序二叉树的先序遍历)
    如何从 100 亿 URL 中找出相同的 URL?
    Tomcat源码分析 | 一文详解生命周期机制Lifecycle
    SqlSession与SqlSessionFactory到底是什么关系?
    spring boot-jpa整合QueryDSL来简化复杂操作
    EasyExcel读写Excel
    如何将List集合中相同属性的对象合并
    @Data 注解引出的 lombok
    MySQL配置连接
    Django创建
  • 原文地址:https://www.cnblogs.com/hzoi-liujiahui/p/13726880.html
Copyright © 2020-2023  润新知