• 图论小专题B


    2 树

    2.1 树的定义

    一个只有(N-1)条边,且任意两个点连通的图叫做树。通过这样定义的树往往是一棵无根树,而我们通常会任意选定一个根节点使其变成有根树。有根树可以定义“父亲和儿子”的层次关系,这往往有利于构造最优子结构,进行DP和搜索等操作。

    特别的,如果在树上任意加上一条边,那么整个树上就会多出一个环。我们称这样的树是“基环树”。基环树不是树,但是它只有一个环。将整个环作为一个“广义根”,然后将根和连在环上的子树分开处理,同样可以套用树的许多算法。

    2.2 树上的DP算法

    通常选定一个根,然后用DFS计算。至于递归接口应该放在转移之前还是之后呢?那就看方程怎么写了。在写程序的时候,只要满足“已知推未知”的原则就行。

    如果给定一棵无根树,答案要求给出最优的根使得某个值最优化,这时可以采用“换根法”。先任意选定一个根计算出规划值(F_1),然后从数学上推导出以任意点为根的规划值(F_2)。《进阶指南》上有相关的例题。

    2.2.1 树的参量

    子树大小size
    最基础的量。转移方程简记为(F(x)=1+sum F( ext{son}(x)))
    树的重心
    和size一样。如果子树(x)的大小是( ext{size}(x)),那么剩下树的大小就是(N- ext{size}(x))。在求( ext{size})时可以顺带求出。
    树的直径
    有一种DP方程,还有一种搜索方法。

    第一种方法,设(F_1(x))表示点(x)到它的子树最长的距离。有方程:

    [ F_1(x)=max_{y in ext{son}(x)}{F(y)+d(x,y)} ]

    然后以此推导出经过(x)的,在(x)子树内的最长链。设它为(F_2(x))
    根据定义,我们在子树里面找出两条过(x)的路径,这两条路径最长即符合要求。有方程:

    [ F_2(x)=max_{y_1,y_2in ext{son}(x)}{F_1(y_1)+d(x,y_1)+F_1(y_2)+d(x,y_2)} ]

    这两个值,一个一定是最大值,一个一定是次大值。我们根据(F_1)的定义,得到:

    [ F_2(x)=max_{y_2in ext{son}(x),y_2 eq y_1,F_1(x)=F(y_1)+d(x,y_1)}{F_1(x)+F_1(y_2)+d(x,y_2)} ]

    这样方程会相当麻烦。我们换一种思路:
    (F_1(x))表示(x)(x)子树叶子的最大距离,(G_1(x))表示次大距离。这样我们有两个方程:

    [egin{cases} G_1(x)=F_1(x),F_1(x)=F_1(y)+d(x,y) & F_1(x) < F_1(y)+d(x,y)\ G_1(x)=F_1(y)+d(x,y) & ext{else if }G_1(x)<F_1(y)+d(x,y) end{cases}\ y in ext{son}(x) ]

    这样(F_2(x)=G_1(x)+F_1(x))就是原来所求了。枚举最大的(F(i))即可求得答案。

    也可以用两次BFS或DFS。先任意一个点(root),搜索出离(root)最远的点(p_1);然后再搜索出离(p)点最远的点(p')。那么两点的距离(pp')就是树的直径。

    2.2.2 LCA

    如果节点(u)既是(x)的祖先,又是(y)的祖先,则(u)(x,y)的公共祖先。当这个公共祖先深度最深时,记(u=LCA(x,y))(x,y)的最近公共祖先。
    求LCA有若干种方法:

    向上标记法(O(qN))
    对于要求的(LCA(x,y)),我们先选一个节点(x)走到根节点,将路径上的点全部标记。然后,我们再让(y)同时往上走,(y)遇到的第一个被标记的点就是(LCA(x,y))

    树上倍增法(O(qlog N))
    首先我们令(F(x,i))表示(x)(2^i)辈祖先,也就是(x)往上走(2^i)步得到的节点。初始时有(F(x,0)= ext{father}(x)),然后以(i)为阶段,以(F(x,i)=F(F(x,i - 1), i - 1))为转移方程,就可以处理出所有的(F(x,i))

    接下来我们选择一个点往上跳(设这个点为(x))。可以进行交换使得( ext{deep}(x)geq ext{deep}(y))。依次尝试让(x)向上走(2^{log N},cdots,2^2,2^1,2^0)步,使得每一步都恰好满足( ext{deep}(x) geq ext{deep}(y))。最后一步应有( ext{deep}(x)= ext{deep}(y))。如果有(x=y),那么(LCA(x,y)=y)

    否则,我们让(x)(y)同时向上跳(2^{log N},cdots,2^2,2^1,2^0)步,使得每一步都有(x eq y)。在最后一步的时候,一定有( ext{father}(x)= ext{father}(y)=LCA(x,y))

    树上倍增法有非常广泛的应用。举个例子,有道题就需要维护树上路径的最大值,这时就可以用“树上ST表”。这个结构就是树上倍增法的体现。

    树链剖分(O(qlog N))
    重点是用两次dfs处理出top数组,即每条树链的顶端。每次询问时,如果(x)(y)都在同一条链上,则(LCA(x,y))就是深度较小的那个节点。否则,我们就让深度较大的节点往它的上面的链跳,即令(x= ext{father}( ext{top}(x)))

    实现起来代码长度较长,会有一定常数,但是在时间复杂度上面应该还是略优于倍增法。且这种方法可扩展性强,可以配套其他的操作。

    LCA的Tarjan算法(O(q+N))
    用并查集对向上标记法的优化。

    在DFS搜索一棵树时,每个节点有三个状态:UNMARKEDTRAVERSALBACKTRACKED,分别表示“未标记”,“已遍历”,“已回溯”。
    当一个节点(x)正处于TRAVERSAL状态时,其沿着父亲至根节点一定有一条TRAVERSAL链。此时对于任意一个处于BACKTRACKED状态的节点(y)(LCA(x,y))就是(y)沿着父亲路径,遇到的第一个TRAVERSAL节点。这是向上标记法的实质。
    这里对于UNMARKED(y)是不成立的,因为TRAVERSAL链和BACKTRACKED链的交点相当于一个不同时刻决策的分支,是第一个使得(x)(y)分立的节点。否则,沿着(y)的一段路径会标记成TRAVERSAL,而不是BACKTRACKED

    在这里,我们用一个并查集来维护这个路径。对于一个BACKTRACKED节点,我们定义它的支点(top(y))表示节点(y)沿着父亲路径,向上遇到的第一个TRAVERSAL节点。
    当一个节点(x)TRAVERSAL变成BACKTRACKED时,它的父亲一定是TRAVERSAL的(根据遍历回溯的顺序可以得到)。这时一定有(top(x)=top( ext{father}(x)))
    当我们在访问(top(y))时,可以顺便进行路径压缩,即(top(y)=top(top(y)))。这对答案是没有影响的。这一点和并查集类似,可以用并查集的get操作完成。

    总结一下,对于每一个UNMARKED节点(x),我们先标记为TRAVERSAL,并遍历它的儿子(y),然后令(top(y)=x)。随后,对于每一个和(x)有关的询问(LCA(u_i,x)),如果(u_i)BACKTRACKED的,我们可以直接由get(u[i])得到答案。
    将询问离线处理,预处理和每个节点有关的询问,然后运行这个算法。时间复杂度(O(q+N)),其中并查集合并的时间复杂度可以忽略不计。

    
    unsigned short state[MAXN];
    #define UNMARKED 0
    #define TRAVERSAL 1
    #define BACKTRACKED 2
    
    int top[MAXN];
    inline int get(int cur)
    {
    	if(cur == top[cur])
    		return cur;
    	return top[cur] = get(top[cur]);
    }
    inline void init()
    {
    	for(rg int i = 1; i <= N; ++ i)
    		top[i] = i;
    }
    
    void DFS(int cur)
    {
    	state[cur] = TRAVERSAL;
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(state[to] != UNMARKED)
    			continue;
    		DFS(to);
    		top[to] = cur;
    	}
    	
    	for(rg int i = 0; i < queryNode[cur].size(); ++ i)
    	{
    		int node = queryNode[cur][i], rank = queryRank[cur][i];
    		if(state[node] != BACKTRACKED)
    			continue;
    		ans[rank] = get(node);
    	}
    	state[cur] = BACKTRACKED;
    }
    

    2.3 最小生成树(MST)

    2.3.1 最小生成树算法

    最小生成树是某个图的子图。它是一棵树,且边权之和最小。在构造最小生成树时,我们可以尽可能贪心地选取边权小的边。这样就有了第一个算法:

    Kruskal算法

    简单来说,我们每次在整个图中选取未被选取的,局部权值最小的边。如果加入这条边,可以让原来不连通的两个森林连通,就把这条边加入到生成树中。否则,我们就跳过这条边,继续检查下一条边。

    可以对边先快速排序,然后用并查集维护点和点的连通性。时间复杂度(O(Mlog M))

    Prim算法
    (S)(T)分别表示待选集合和已选集合。在最开始时,(T={1})
    (iin S)我们设(dis_i)是点(i)到集合(T)中最近点的距离。每次我们选取最小的(dis_i),然后将(i)选入(T)中,同时用(i)更新其他点的(dis)值。

    2.3.2 衍生算法

    Kruskal重构树
    来源自一个非常简单的模型:

    • 求无向连通图中,两点之间所有简单路径的最大边权的最小值。

    也就是说,两点之间有若干条路径,而每条路径上都有一个最大边权。求这些最大边权中的最小值。

    在执行Kruskal算法时,我们会依次选取边权最小的边,然后将边对应的两个连通块合并。在这里,我们不是直接合并,而是设定一个“虚点”,让两个连通块都指向这个虚点。虚点的权值就是原来的边权。这样,两点之间最小的最大边权就是合并两个点所属连通块的所需边权,重构后,也就是两点的(LCA)的点权。

    这样,上面这道题就可以用(O(log N))的时间处理每一个询问了。

    可以结合NOI2018 归程来具体理解一下。这里是这道题的题解
    如果你认为这道题有点难,不妨试一下这道题:NOIP2013 货车运输。这道题可以通过构造一个最大生成树的重构树直接完成。

    2.4 树链剖分

    这部分内容最好结合《一本通》上面的图理解。这里只快速捋一捋知识点。
    树链剖分其实在考场上是一种非常有风险的算法,因为代码量大,会消耗大量的时间和精力。不过,有些题目还是必须得用这种方法求解。

    树链剖分的主要方式为重链剖分。当然,长链剖分也是一种形式。所谓的重链就是对于每一个节点,连接子树大小最大的儿子,从而形成一条链。剖分的时候,所有的“重儿子-父亲”边会形成一条条链,这些链按顺序排列就会形成一个区间。所谓的重儿子,就是指子树大小最大的儿子。

    在区间上就可以套用区间数据结构了。当然,这里主要是介绍“线段树+树链剖分”的方法。

    在树链剖分时,我们先要用DFS预处理出所有的点参量,包括father depth size son,即父亲,深度,子树大小,重儿子。这部分的代码如下:

    inline void getSon(int cur, int curFather)
    {
    	father[cur] = curFather;
    	depth[cur] = depth[curFather] + 1;
    	size[cur] = 1;
    	son[cur] = 0;
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(to == curFather)
    			continue;
    		
    		getSon(to, cur);
    		size[cur] += size[to];
    		if(son == 0 || size[to] > size[son[cur]])
    			son[cur] = to;
    	}
    }
    

    接下来,我们给每个节点打上时间戳,并且对时间戳和节点建立一一对应的关系。用dfn[x]表示点x的时间戳,用rev[i]表示时间戳为i的节点编号。
    同时,我们预处理出每个节点x所在的重链的顶端top[x]。对于重儿子,有top[son[x]]=top[x];对于其他的儿子,它们单独作为一条新链的开端,有top[to]

    inline void getTop(int cur, int curFather)
    {
    	dfn[cur] = ++ timeStamp;
    	rev[timeStamp] = cur;
    	
    	if(son[cur])
    	{
    		top[son[cur]] = top[cur];
    		getTop(son[cur], cur);  
    	}
    	
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(to == curFather || to == son[cur])
    			continue;
    		top[to] = to;
    		getTop(to, cur);
    	}
    }
    

    在每一条链上,深度depth大的节点时间戳一定大。这样,树上的每个点就被一一对应到[1,timeStamp]的闭区间里了。

    接下来以询问最大值为例,演示一下如何询问树上两点间路径上的信息。
    为了方便封装,我们假定我们定义了这样的一个结构体:

    SegmentTree sgt;
    

    数据结构sgt是一棵建立在区间[1,timeStamp]上的线段树,可以通过成员函数getMax(int left, int right)访问区间[left, right]的最大值。对于路径的两个端点u v,我们进行这样的操作:

    1. uv在同一条链上,我们先交换使得depth[u]<=depth[v],然后我们直接用sgt.getMax(dfn[u], dfn[v])来更新答案,随后结束过程。
    2. 否则,我们每次选择链深度大的点(假定我们交换两点使得depth[top[u]] >= depth[top[v]] ),然后用sgt.getMax(dfn[top[u]], dfn[u])更新答案。之后我们让u = father[top[u]]使u转至下一条链上。如果此时``u v` 仍不在同一条链上,则执行2。反之,则执行1。

    这部分代码如下:

    number cur = -INF;								
    int u = read(1), v = read(1);                   
    int fu = top[u], fv = top[v];                   
    while(fu != fv)                                 
    {                                               
    	if(depth[fu] < depth[fv])                   
    	{                                           
    		swap(u, v);                             
    		swap(fu, fv);                           
    	}                                           
    	checkMax(cur, sgt.getMax(dfn[fu], dfn[u])); 
    	u = father[fu];                             
    	fu = top[u];                                
    }                                               
    if(depth[u] > depth[v])                         
    	swap(u, v);                                 
    checkMax(cur, sgt.getMax(dfn[u], dfn[v]));      
                                                    
    printf("%lld
    ", cur);
    

    这里张贴ZJOI 2008 树的统计的代码。这道题要求支持动态查询树上两点间的点权和和点权最大值。代码如下:

    #include <cstdio>
    #include <cctype>
    #include <cstring>
    using namespace std;
    #define rg register
    #define fre(z) freopen(z".in", "r", stdin), freopen(z".out", "w", stdout)
    #define customize template<class type> inline
    typedef long long number;
    const number INF = 0x3f3f3f3f3f3f3f3f;
    customize type read(type sample)
    {
    	type ret = 0, sign = 1; char ch = getchar();
    	while(! isdigit(ch))
    		sign = ch == '-' ? -1 : 1, ch = getchar();
    	while(isdigit(ch))
    		ret = ret * 10 + ch - '0', ch = getchar();
    	return sign == -1 ? -ret : ret;
    }
    
    const int MAXN = 30010;
    
    int N, Q;
    int timeStamp = 0;
    
    int head[MAXN];
    struct Edge{
    	int next;
    	int front, to;
    }edge[MAXN << 1];
    int tot = 0;
    inline void append(int front, int to)
    {
    	++ tot;
    	edge[tot] = (Edge){head[front], front, to};
    	head[front] = tot;
    }
    inline void connect(int front, int to)
    {
    	append(front, to);
    	append(to, front);
    }
    
    int dfn[MAXN], rev[MAXN];
    int father[MAXN], depth[MAXN], size[MAXN], top[MAXN], weight[MAXN], son[MAXN];
    
    inline void getSon(int cur, int curFather)
    {
    	father[cur] = curFather;
    	depth[cur] = depth[curFather] + 1;
    	size[cur] = 1;
    	son[cur] = 0;
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(to == curFather)
    			continue;
    		
    		getSon(to, cur);
    		size[cur] += size[to];
    		if(son == 0 || size[to] > size[son[cur]])
    			son[cur] = to;
    	}
    }
    
    inline void getTop(int cur, int curFather)
    {
    	dfn[cur] = ++ timeStamp;
    	rev[timeStamp] = cur;
    	
    	if(son[cur])
    	{
    		top[son[cur]] = top[cur];
    		getTop(son[cur], cur);  
    	}
    	
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(to == curFather || to == son[cur])
    			continue;
    		top[to] = to;
    		getTop(to, cur);
    	}
    }
    
    customize type max(type a, type b)
    {
    	return a > b ? a : b;
    }
    customize type checkMax(type &var, type value)
    {
    	return var = var > value ? var : value;
    }
    customize void swap(type &a, type &b)
    {
    	type temp = a;
    	a = b;
    	b = temp;
    }
    
    struct SegmentTree{
    	struct Node{
    		int left, right;
    		int lc, rc;
    		struct Data{
    			number max;
    			number sum;
    		}data;
    		number add;
    		inline int len()
    		{
    			return right - left + 1;
    		}
    	}node[MAXN << 2];
    	int size;
    	int root;
    	SegmentTree()
    	{
    		size = 0;
    	}
    	inline int newNode(int left, int right, int val)
    	{
    		++ size;
    		node[size] = (Node){left, right, 0, 0, (Node::Data){val, val}, 0};
    		return size;
    	}
    	inline int build(int left, int right)
    	{
    		if(left == right)
    		{
    			return newNode(left, right, weight[rev[left]]);
    		}
    		
    		int mid = (left + right) >> 1;
    		int cur = newNode(left, right, 0);
    		node[cur].lc = build(left, mid);
    		node[cur].rc = build(mid + 1, right);
    		node[cur].data.sum = node[node[cur].lc].data.sum + node[node[cur].rc].data.sum;
    		node[cur].data.max = max(node[node[cur].lc].data.max, node[node[cur].rc].data.max);
    		return cur;
    	}
    	inline void init(int left, int right)
    	{
    		root = build(left, right);
    	}
    	inline void spread(int cur)
    	{
    		if(node[cur].add)
    		{
    			number curAdd = node[cur].add;
    			node[node[cur].lc].data.sum += node[node[cur].lc].len() * curAdd;
    			node[node[cur].rc].data.sum += node[node[cur].rc].len() * curAdd;
    			node[node[cur].lc].data.max += curAdd;
    			node[node[cur].rc].data.max += curAdd;
    			node[node[cur].lc].add = curAdd;
    			node[node[cur].rc].add = curAdd;
    			node[cur].add = 0;
    		}
    	}
    	inline number searchMax(int cur, int left, int right)
    	{
    		if(left <= node[cur].left && node[cur].right <= right)
    		{
    			return node[cur].data.max;
    		}
    		spread(cur);
    		number temp = -INF;
    		if(left <= node[node[cur].lc].right)
    		{
    			checkMax(temp, searchMax(node[cur].lc, left, right));
    		}
    		if(right >= node[node[cur].rc].left)
    		{
    			checkMax(temp, searchMax(node[cur].rc, left, right));
    		}
    		return temp;
    	}
    	inline number getMax(int left, int right)
    	{
    		return searchMax(root, left, right);
    	}
    	inline number searchSum(int cur, int left, int right)
    	{
    		if(left <= node[cur].left && node[cur].right <= right)
    		{
    			return node[cur].data.sum;
    		}
    		spread(cur);
    		number temp = 0;
    		if(left <= node[node[cur].lc].right)
    		{
    			temp += searchSum(node[cur].lc, left, right);
    		}
    		if(right >= node[node[cur].rc].left)
    		{
    			temp += searchSum(node[cur].rc, left, right);
    		}
    		return temp;
    	}
    	inline number getSum(int left, int right)
    	{
    		return searchSum(root, left, right);
    	}
    	inline void change(int cur, int pos, number val)
    	{
    		if(node[cur].left == node[cur].right)
    		{
    			node[cur].data = (Node::Data){val, val};
    			return;
    		}
    		spread(cur);
    		if(pos <= node[node[cur].lc].right)
    		{
    			change(node[cur].lc, pos, val);
    		}
    		if(pos >= node[node[cur].rc].left)
    		{
    			change(node[cur].rc, pos, val);
    		}
    		node[cur].data.max = max(node[node[cur].lc].data.max, node[node[cur].rc].data.max);
    		node[cur].data.sum = node[node[cur].lc].data.sum + node[node[cur].rc].data.sum;
    	}
    	inline void change(int pos, number val)
    	{
    		change(root, pos, val);
    	}
    }sgt;
    
    int main()
    {
    	N = read(1);
    	for(rg int i = 1; i < N; ++ i)
    	{
    		int u = read(1), v = read(1);
    		connect(u, v);
    	}
    	for(rg int i = 1; i <= N; ++ i)
    	{
    		weight[i] = read(1ll);
    	}
    	
    	getSon(1, 0);
    	
    	top[1] = 1;
    	getTop(1, 0);
    	
    	sgt.init(1, timeStamp);
    
    	Q = read(1);
    	for(rg int i = 1; i <= Q; ++ i)
    	{
    		char op[10];
    		scanf("%s", op);
    		if(op[0] == 'C')
    		{
    			int u = read(1);
    			number val = read(1ll);
    			sgt.change(dfn[u], val);
    		}
    		else
    		{
    			if(op[1] == 'M')
    			{
    				number cur = -INF;								
    				int u = read(1), v = read(1);
    				int fu = top[u], fv = top[v];
    				while(fu != fv)
    				{
    					if(depth[fu] < depth[fv])
    					{
    						swap(u, v);
    						swap(fu, fv);
    					}
    					checkMax(cur, sgt.getMax(dfn[fu], dfn[u]));
    					u = father[fu];
    					fu = top[u];
    				}
    				if(depth[u] > depth[v])
    					swap(u, v);
    				checkMax(cur, sgt.getMax(dfn[u], dfn[v]));
    
    				printf("%lld
    ", cur);							
    			}
    			else
    			{
    				number cur = 0;
    				int u = read(1), v = read(1);
    				int fu = top[u], fv = top[v];
    				while(fu != fv)
    				{
    					if(depth[fu] < depth[fv])
    					{
    						swap(fu, fv);
    						swap(u, v);
    					}
    					cur += sgt.getSum(dfn[fu], dfn[u]);
    					u = father[fu];
    					fu = top[u];
    				}
    				if(depth[u] > depth[v])
    					swap(u, v);
    				cur += sgt.getSum(dfn[u], dfn[v]);
    
    				printf("%lld
    ", cur);
    			}
    		}
    	}
    	return 0;
    }
    

    2.5 树上差分

    “差分”这个概念源于序列操作。部分区间问题需要对整段区间进行大量的修改,而最后进行询问。如果采用暴力做法,修改的时间复杂度是(O(N))的,而查询的时间复杂度仅为(O(1))。对于只询问一次的题目,这种做法是非常不平衡的。

    我们先来看一个模型:

    给定(Q)个操作,每次标记一个区间([l, r])。最后询问每个位置(i)被标记的次数。

    假设(A_i)表示(i)位置被标记的次数,(A_0 = 0),那么我们可以令(Delta A_i = A_i - A_{i - 1})。这样做的好处在于修改的时间复杂度大大降低:假设(Delta A_i) 原来均为(0),现在我们在([l, r])上打标记,那么仅有(Delta A_l = A_l - A_{l - 1} = 1)(Delta A_{r+1} = A_{r + 1} - A_r = -1)两个位置发生了改变。令(Delta A_l = 1)(Delta A_{r+1} = -1)即可。
    在询问的时候,直接查询差分数组的前缀和(A_i = sum_{j = 1}^{i} Delta A_j)即可。

    我们把序列上的差分推广到树上,可以得到这样的模型:

    给定多个操作,每次在树的两点之间的路径进行标记。求每个点(边)被标记的次数。

    树上的前缀和比区间要稍微复杂一点。我们以点为例,介绍一下如何进行树上差分。
    我们设(F(x))表示当前节点被经过的次数。如果我们标记一条路径,我们可以把这个路径拆成
    两段:(u ightarrow LCA(u,v) ightarrow v)。当然,这其中也包含了(LCA(u,v) in {u,v})的情况。这条路径上每一个(F(x))都要自增1。

    我们考虑一种最暴力的更改做法:令(F(u))向上走到(F(root))的部分均自增1,然后让(F(v))向上走到(F(root))的部分也均自增1。由于LCA被重复计算了一次,所以(F(LCA))向上走到(F(root))的部分均自减1。而(LCA)以上的部分不能被计算,所以令(F( ext{father}(LCA)))(F(root))的部分均自减1。
    这样做的好处是易于差分。我们令(Delta F(x) = F(x) - sum_{ ext{son } y}F(y))
    当每次加入一条树上的路径((u,v))时,我们令(Delta F(u)++)(Delta F(v)++)(Delta F(LCA)--)(Delta F( ext{father}(LCA))--)
    这样就和原来的暴力做法一一对应起来了。在最后统计答案时,我们只要DFS统计子树的(Delta F)和,就可以得到原来的(F)了。

    可以结合这份代码片段理解一下:

    for(rg int i = 1; i <= K; ++ i)     
    {                                   
    	int u = read(1), v = read(1);   
    	int lca = LCA(u, v);            
    	++ DF[u]; ++ DF[v];             
    	-- DF[lca]; -- DF[father[lca]]; 
    }                                   
    getMax(1, 0);                       
    printf("%d", ans);					
    

    DFS部分:

    inline void getMax(int cur, int curFather)
    {
    	sum[cur] = DF[cur];
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(to == curFather)
    			continue;
    		getMax(to, cur);
    		sum[cur] += sum[to];
    	}
    	checkMax(ans, sum[cur]);
    }
    

    2.6 点分治

    点分治是树分治的一种。它的核心原理在于:

    • 根的选取对于答案没有影响
    • 可以划分出子结构,递归计算

    比较典型的一类问题是统计树上的路径数。直接暴力枚举显然是不可取的,我们考虑路径的种类:

    1. 端点存在家族关系,即某个点是另一个点的祖父
    2. 其他

    对于这两种情况,我们都可以找到一个根节点,使得路径经过这个根节点。这样一来,我们就把“统计整棵树的答案”这个问题划分成了“统计子树的答案”。

    如何递归地进行求解呢?我们可以考虑每次先计算父节点,然后依次计算各个子节点。这样一来,整个计算的顺序就会呈现出一个树形结构,时间复杂度就会从(N)(log N)级别靠近。

    当然,如果树退化成了一条链,而我们恰好又从端点开始计算。解决方案是对于当前子树,我们先找到这棵树的重心,然后以重心为根计算这棵子树的答案。

    不过点分治确实对于我来说太难了。我对着各种题解看了好几天,可能我的代码实现水平还不够高吧。这里就直接张贴洛谷模板题的代码了。这道题要求询问树上是否存在长度为(K)的路径。

    #include <cstdio>
    #include <cstring>
    #include <cctype>
    #include <vector>
    using namespace std;
    #define rg register
    #define fre(z) freopen(z".in", "r", stdin), freopen(z".out", "w", stdout)
    #define customize template<class type> inline
    typedef long long number;
    const number INF = 0x3f3f3f3f3f3f3f3f;
    customize type read(type sample)
    {
    	type ret = 0, sign = 1; char ch = getchar();
    	while(! isdigit(ch))
    		sign = ch == '-' ? -1 : 1, ch = getchar();
    	while(isdigit(ch))
    		ret = ret * 10 + ch - '0', ch = getchar();
    	return sign == -1 ? -ret : ret;
    }
    
    const int MAXN = 100010;
    const int MAXM = 110;
    
    int N, M; number K[MAXM];
    int head[MAXN];
    struct Edge{
    	int next;
    	int front, to;
    	number len;
    }edge[MAXN << 1];
    int tot = 0;
    inline void append(int front, int to, number len)
    {
    	++ tot;
    	edge[tot] = (Edge) {head[front], front, to, len};
    	head[front] = tot;
    }
    inline void connect(int front, int to, number len)
    {
    	append(front, to, len);
    	append(to, front, len);
    }
    
    int size[MAXN];
    bool used[MAXN];
    bool exist[10000010];
    bool sat[MAXM];
    
    int root;
    int minSize;
    
    inline void getRoot(int cur, int father, int curSize)
    {
    	size[cur] = 1;
    	int maxPart = 0;
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(used[to] || to == father)
    			continue;
    		getRoot(to, cur, curSize);
    		size[cur] += size[to];
    		if(size[to] > maxPart)
    			maxPart = size[to];
    	}
    	if(maxPart > curSize - size[cur])
    		maxPart = curSize - size[cur];
    	if(maxPart <= minSize)
    	{
    		root = cur;
    		minSize = maxPart;
    	}
    }
    
    number dis[MAXN];
    vector<number> subdis;//the distance in the current subtree
    vector<int> curdis;//the distance that have been calculate in the current dividing procedure
    
    inline void getDis(int cur, int father)
    {
    	subdis.push_back(dis[cur]);
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(used[to] || to == father)
    			continue;
    		dis[to] = dis[cur] + edge[e].len;
    		getDis(to, cur);
    	}
    }
    
    inline void calc(int cur)
    {
    	curdis.clear();
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		subdis.clear();
    		if(used[to])
    			continue;
    		dis[to] = edge[e].len;
    		getDis(to, cur);
    		
    		for(rg int i = subdis.size() - 1; i >= 0; -- i)
    			for(rg int j = 1; j <= M; ++ j)
    				sat[j] |= exist[(int) K[j] - subdis[i]];
    		
    		//ask whether there's a path exist in the current subtree.
    		//we must make sure that the endpoints of the path don't both belong to the same subtree.
    		
    		for(rg int i = 0; i < subdis.size(); ++ i)
    		{
    			curdis.push_back(subdis[i]);
    			exist[(int) subdis[i]] = true;
    		}
    	}
    	for(rg int i = 0; i < curdis.size(); ++ i)
    		exist[curdis[i]] = 0;
    }
    inline void devide(int cur)
    {
    	used[cur] = 1;//lable this node, representing that this node has been deleted.
    	exist[0] = 1;// the current node has a path leading to itself with the length 0
    	calc(cur);//calculate and get the statiscal information
    	for(rg int e = head[cur]; e; e = edge[e].next)
    	{
    		int to = edge[e].to;
    		if(used[to])
    			continue;
    		root = 0; minSize = N + 1;
    		getRoot(to, 0, size[to]);//find the center of gravity of the root.
    		devide(to);//solve with the depth-first order
    	}
    }
    
    int main()
    {
    	fre("tree");
    	N = read(1); M = read(1);
    	for(rg int i = 1; i < N; ++ i)
    	{
    		int u = read(1), v = read(1); number l = read(1ll);
    		connect(u, v, l);
    	}
    	for(rg int i = 1; i <= M; ++ i)
    		K[i] = read(1ll);
    	
    	minSize = N;
    	getRoot(1, 0, N);
    	devide(root);
    	
    	for(rg int i = 1; i <= M; ++ i)
    	{
    		if(sat[i])
    			puts("AYE");
    		else
    			puts("NAY");
    	}
    	return 0;
    }
    
  • 相关阅读:
    JAVA并发之ReentrantLock源码(一)
    java并发之线程池
    Quine--输出程序源码的程序(java)
    【leetcode】Weekly Contest 92
    【java集合类】ArrayList和LinkedList源码分析(jdk1.8)
    【leetcode】Weekly Contest 91
    牛客2018.6模拟考编程题
    MFC 完全自定义控件
    图形学中求平面方程系数以及法向量
    std::function解决函数重载绑定
  • 原文地址:https://www.cnblogs.com/LinearODE/p/11324746.html
Copyright © 2020-2023  润新知