首页 > 其他分享 >[数据结构]树上倍增

[数据结构]树上倍增

时间:2023-08-15 21:35:00浏览次数:35  
标签:cnt 数据结构 int 倍增 long 权值 树上 mod

树上倍增

一、一点理解

最近遇到几个关于树上倍增问题。本人太菜,有时候想不到用倍增,这里做个总结。

树上倍增核心就是:\(f[u][j]\),含义是\(u\)向上跳\(2^j\)步到的位置,然后用\(f[u][j] = f[f[u][j-1]][j-1]\)进行转移。

树上倍增常见应用就是:快速幂、线性(RMQ问题)、树(LCA问题)。

那么我们怎么去理解这个树上倍增呢?

由:介个大佬写的博客引发的思考。可以将倍增理解为【树上二分】

对于一颗树,我们需要先预处理出"二分"的位置

怎么说呢,未知上界的二分可以考虑倍增

一个快捷的对取方法:

int logn = 31 - __builtin_clz(n); 

二、比如以下几个题:

先说一个常见模型

给出一个长度为\(n\)的环和一个常数\(k\),每次会从第\(i\)个点跳到第\((i+k)\mod n+1\)个点,总共跳\(m\)次,每个点都有一个权值\(a_i\),求\(m\)次跳跃的权值和(结果对\(1e9+7\)取模。

思路:首先\(m\)是\(1e18\)级别的,显然不能直接暴力跳了。考虑优化,思考我们上面说的【未知上界的二分考虑倍增】。那么我们需要进行预处理,考虑一个点跳\(2^j\)步能到的位置,以及跳\(2^j\)步的权值和。

\(p[i][j] = p[p[i-1][j-1]][j-1]\)

\(val[i][j] += val[p[i-1][j-1]][j-1]\)

那么对于跳\(m\)步:

int cnt = 0;
int curx = 1;
while(m)
{
    if(m&1)
    {
        ans = (ans+sum[curx][cnt])%mod;
        curx = p[curx][cnt];
	}
    m>>=1;
    cnt++;
}

例题1:1010 Calculate

题意:给你一个\(n\)个点\(n\)条边的图,一个点只能通过唯一一条边到另一个点

一开始的值为\(y\),当到达点\(i\)的时候,值变为\(y\times k_i+b_i\)。

给你\(q\)个询问,问你从\(x\)点出发,走\(l\)步,一开始的值是\(y\),问走\(l\)步之后的值是多少。

思路:当时看到这个题,由于\(l\)的范围是\(1e9\)太大了,直接暴力走会寄。我就想到,对于\(n\)个点\(n\)条边,那必然是有环的,然后想要怎么处理这个环。最初的想法是,对于每个点,处理出可以走的路径,有环的话把环展成链,因为一旦进入环就走不出去了,就会一直循环,我们对循环节进行处理,可以解决时间复杂度的问题。但是!这样显然是错的,首先,因为每一次移动之后值是会变的,移动一个循环的增量太复杂。。根本无非求解。

转化思路!!再看看题目!当前在\(x\)点,我们取往后跳,每次能够跳到的点是唯一的,也就是有唯一父子关系。那么考虑倍增。先预处理出每个点跳\(2^k\)步的贡献,因为是\(\log\)的复杂度,我们不要考虑环了,暴力跳就行了。

预处理的时候把\(p\)初始为当前点跳\(2^0\)步能到达的点。

\(p[u][j]\):\(u\)往上跳\(2^j\)步能到达的点。

\(f[u][j]\):\(u\)往上跳\(2^j\)步的值,当然\(k,b\)分开算。

\(f[u][j].k = f[u][j-1].k*f[p[u][j-1]][j-1].k\)

\(f[i][j].b = f[i][j-1].b*f[p[i][j-1]][j-1].k+f[p[i][j-1]][j-1].b\)

总结:当一个点能跳到的下一个点是唯一确定的,且步数很大的时候,我们可以考虑用倍增。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int mod = 1e9+7;

struct ty
{
	ll k,b;
}f[N][32];
ll p[N][32],k[N],b[N];

int main()
{
	int t;
	cin>>t;
	while(t--)
	{
		int n,q;
		cin>>n>>q;
		for(int i = 1;i<=n;i++)
			cin>>k[i];
		for(int i = 1;i<=n;i++)
			cin>>b[i];
		for(int i = 1;i<=n;i++)
		{
			cin>>p[i][0];
			f[i][0].k = k[p[i][0]];
			f[i][0].b = b[p[i][0]];
		}

		for(int j = 1;j<=30;j++)
		{
			for(int i = 1;i<=n;i++)
			{
				p[i][j] = p[p[i][j-1]][j-1];
				f[i][j].k = f[i][j-1].k*f[p[i][j-1]][j-1].k%mod;
				f[i][j].b = (f[i][j-1].b*f[p[i][j-1]][j-1].k%mod+f[p[i][j-1]][j-1].b)%mod;
			}
		}
		ll kk = 1,bb = 0;
		ll x,l,y,cnt = 0;
		for(int i = 1;i<=q;i++)
		{
			kk = 1,bb = 0;
			cnt = 0;
			cin>>x>>l>>y;
			while(l)
			{
				if(l&1)
				{
					kk = kk*f[x][cnt].k%mod;
					bb = (bb*f[x][cnt].k%mod+f[x][cnt].b)%mod;
					x = p[x][cnt];
				}
				l>>=1;
				cnt++;
			}
			cout<<(kk*y%mod+bb)%mod<<"\n";
		}
	}
	return 0;
}

例题2:H.Life is a Game

题意:给你\(n\)个点\(m\)条边的无向图。每个点有点权,每条边也有边权。我们有一个初始能量值,每经过一个点,可以获得当前点的点权,但是要经过一条边,需要我们当前的能力值大于这个边的边权才能走。给你起点和初始能量值,问你能量值最大可以是多少?

思路:\(Kruskal\)重构树+树上倍增

由于有边权的限制,当能量值大于当前边权才能走。那么对于一条简单路径,限制我们的是这条路径上的最大值。我们考虑最优策略,如果有多条路,肯定走最大值越小的路。那么想让最大值最小,考虑\(Kruskal\)重构最小生成树。重构完之后,任意两点的\(lca\)就是这个路径上的最大值了。

我一开始的思路是:从当前给出的节点往上走,如果当前的值大于等于限制,我们就可以获得这个子树的所有点的权值。但是\(T7\)了。考虑最坏的情况,二叉树退化成链,这样的话,就是跑的暴力了,肯定是会\(T\)的。那怎么办呢?

因为我们建的是\(Kruskal\)重构树,是个大根堆,考虑预处理出每个点往上跳\(2^j\)步的花费(预处理出"二分"位置)。这里花费是指【要跳到的节点的限制-当前节点子树的权值和】。也就是,当前已经可以走到这里了,那么以当前节点为根的子树的权值我都可以得到了,用我要跳到的点的限制-当前获得的,就是我们的额外的花费。我们用这个花费和\(k\)去比,如果\(\le k\)小就可以往上跳,直到不能跳了为止。那么答案就是你能跳到的点的权值和+初始值\(k\)。

总结:这个题也是,我要往上跳,可是我不知道能跳到哪里。如果暴力跳肯定\(T\),思考上面说的【未知上界的二分考虑倍增】,这样优化到\(\log\)级别的了。

#include<bits/stdc++.h>
#define INF 0x3f3f3f3f
#define int long long 
using namespace std;
typedef long long ll;
const int N = 4e5+10, M = 5e5+10;
const int LOGN = 20;
struct Node
{
	int x,y,v;
}a[M+1];

int n,m,q,now,val[N],dep[N],faa[N];
vector<int>e[N];
int ls[N],rs[N];
int sum[N];
int f[N][LOGN+2],cost[N][LOGN+2];
int c[N];//以i为根的子树的权值和

//////////////////////////////DSU//////////////////////////////////////
struct Dsu {
	int fa[N];
	void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;}
	int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);}
	void merge(int x, int y) {
		int u = find(x), v = find(y);
		if(u == v) return;
		fa[u] = v;
	}
}dsu;
/////////////////////////Kruskal////////////////////////////////////

void kruskalRebuildTree()
{
	dsu.init(2*n-1);
	sort(a+1, a+1+m, [](const Node& x, const Node& y) {
		return x.v < y.v;//重构最小生成树
	});
	now = n;
	for(int i = 1;i<=m;i++) {
		ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v;
		if(u != v) {
			val[++now] = w;
			c[now] = c[u]+c[v];
			cost[u][0] = val[now] - c[u];
			cost[v][0] = val[now] - c[v];
			f[u][0] = f[v][0] = now;
			dsu.merge(u, now);
			dsu.merge(v, now);
			ls[now] = u;
			rs[now] = v;
		}
	}
}


////////////////////////////Main///////////////////////////////////
signed main()
{
	ios::sync_with_stdio(false);	cin.tie(nullptr);cout.tie(nullptr);

	cin>>n>>m>>q;
	for(int i = 1;i<=n;i++)cin>>c[i];
	for(int i = 1;i<=m;i++)
	{
		cin>>a[i].x>>a[i].y>>a[i].v;
	}
	kruskalRebuildTree();
	
	for(int j = 1;j<=LOGN;j++)
	{
		for(int u = 1;u<=now;u++)
		{
			f[u][j] = f[f[u][j-1]][j-1];
			cost[u][j] = max(cost[u][j-1],cost[f[u][j-1]][j-1]);
		}
	}
	while(q--)
	{
		int x, k;
		cin>>x>>k;
		
		for(int j = LOGN;j>=0;j--)
		{
			if(cost[x][j]<=k&&f[x][j])x = f[x][j];
		} 
		cout<<c[x]+k<<"\n";
	}
		
	return 0;
}

标签:cnt,数据结构,int,倍增,long,权值,树上,mod
From: https://www.cnblogs.com/nannandbk/p/17632503.html

相关文章

  • redis数据结构跳表
    redis数据结构跳表数据结构跳表节点typedefstructzskiplistNode{//层structzskiplistLevel{//前进指针structzskiplistNode*forward;//跨度unsignedintspan;}level[];//后退指针structzskiplistNode*backward;//分值doublescore;//成员......
  • 【数据结构】排序2 交换排序
    交换排序就是基于比较交换的排序。主要讲两种交换排序算法:冒泡排序和快速排序。冒泡排序比较简单一般不会单独考察,重点考察的是快速排序的内容。1.冒泡排序基本算法思想:对于每趟排序,从后往前两两比较,如果为逆序则进行交换,这样很显然不能一趟就得到正确的序列,但是每次都会把最......
  • 数据结构4
    算法:数据结构中的算法,指的是数据结构所具备的功能解决特定问题的方法。学习的前辈们的一些优秀的经验总结算法的五大特征:(1)有穷性。一个算法必须总是在执行有穷步后结束,且每一步都必须在有穷时间内完成。(2)确定性。对千每种情况下所应执行的操作,在算法中都有确切的规定,不会......
  • 线性表【数据结构学习-青岛大学王卓老师】
    https://www.bilibili.com/video/BV1qz4y1p767/线性表线性表的初始化(顺序表)StatusInitList(SqList&L){L.elem=(ElemType*)malloc(sizeof(ElemType)*MAXSIZE);if(!L.elem)exit(OVERFLOW);L.length=0;returnOK;}线性表的销毁voi......
  • redis数据结构字典
    redis数据结构字典数据结构Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。哈希表typedefstructdictht{//哈希表数组dictEntry**table;//哈希表大小unsignedlongsize;//哈希表大小掩码,用于......
  • 数据结构(哈夫曼树):判定编码方案是否为前缀编码
    前缀编码定义:(字符集中)任一编码都不是其它字符的编码的前缀(字符集中)任一编码都不是其它字符的编码的前缀(字符集中)任一编码都不是其它字符的编码的前缀重要的话说三遍!例:(1)找出下面不是前缀编码的选项A{1,01,000,001}B{1,01,011,010}C{0,10,110,11}D{0,1,00,11}第一步:看A中的第一个数1,看看其他......
  • redis数据结构链表
    redis数据结构链表数据结构链表节点typedefstructlistNode{//前置节点structlistNode*prev;//后置节点structlistNode*next;//节点的值void*value;}listNode;多个listNode可以通过prev和next指针组成双端链表链表typedefstructlist{//表头节点......
  • redis数据结构sds
    简单字符串sds数据结构structsdshdr{//记录buf数组中已使用字节的数量//等于SDS所保存字符串的长度intlen;//记录buf数组中未使用字节的数量intfree;//字节数组,用于保存字符串charbuf[];};特性空间预分配空间预分配用于优化SDS的字符串年增长操作:当SD......
  • 考研数据结构——每日一题[哈希表]
    840.模拟散列表维护一个集合,支持如下几种操作:Ix,插入一个数x;Qx,询问数x是否在集合中出现过;现在要进行N次操作,对于每个询问操作输出对应的结果。输入格式第一行包含整数N,表示操作数量。接下来N行,每行包含一个操作指令,操作指令为Ix,Qx中的一种。输出格式对于每......
  • Redis设计与实现——数据结构(二刷)
    SDS动态字符串Redis是c语言实现的,传统c字符串存在不可变导致的频繁内存分配,一些API函数可能引起缓冲区溢出等问题。Redis在c字符串的基础上,封装实现了SDS动态字符串,能够根据每次存储关键字的大小自动申请额外缓冲区内存,避免频繁申请和缓冲区溢出问题。SDS定义str......