• 《树上差分》小题两则


    边差分:352. 闇の連鎖

    题意
    给定 n-1 条 主要边 构成一棵树,然后给定 m 条 附加边,每条附加边连接树中两个节点。
    每次需要选择一个主要边和一个附加边删掉,将整个图分成两部分。
    问,一共有多少种选择方案?

    N ≤ 1 0 5 ,   M ≤ 2 ∗ 1 0 5 ,数据保证答案不超过 2 31 − 1 N≤10^5,\ M≤2*10^5,数据保证答案不超过2^{31}−1 N105, M2105,数据保证答案不超过2311

    思路
    对于一条附加边来说,只有其所在环上的一条主要边被砍掉,还要保证这条主要边只在这一个环上,或者是不在环中的一条主要边被砍掉,这个附加边才有贡献,不好处理。
    正难则反!
    反过来考虑主要边:
    如果一条主要边砍掉后能够将整个图分成两部分,那么还要砍掉其所在环中的所有附件边。

    • 如果其只在一个环中,那么将其附加边砍掉即可,贡献为1;
    • 如果其在多个环上,那么需要将这些环的所有附加边都砍掉,但是只能砍掉一个,所以该主要边没有贡献。
    • 如果其不在环上,那么图已经不连通,随意砍掉一个附加边,贡献为附加边个数 m。

    如何判断一个主要边在几个环中呢?

    所有主要边构成了一棵树,正因为加了附加边才有了环,只要加附加边就会构成环。
    环上的主要边为主要边构成的树中,附加边两端点到其 lca 的这条路径。每次都将环上所有边的标记数+1,这样最后就能知道每个边在多少环中。

    而将树上一条链中所有边的权值+1,用到树上差分。
    这里是边差分,将每条边都附着在下面的点上,将两端点的差分值分别+1,然后将其 lca 的差分值-2。然后求所有点所在子树的差分值之和恢复原值,就得到了附着在该点上的新边权。

    Code

    #include
    using namespace std;
    
    #define Ios ios::sync_with_stdio(false),cin.tie(0)
    
    const int N = 200010, mod = 1e9+7;
    int T, n, m;
    int a[N], b[N];
    int dep[N], f[N][30], k;
    vector<int> e[N];
    int ans;
    
    void bfs()
    {
    	dep[0] = 0, dep[1] = 1;
    	queue<int> que;
    	que.push(1);
    	
    	while(que.size())
    	{
    		int x = que.front(); que.pop();
    		
    		for(int tx : e[x])
    		{
    			if(tx == f[x][0]) continue;
    			
    			dep[tx] = dep[x] + 1;
    			que.push(tx);
    			
    			f[tx][0] = x;
    			for(int i=1;i<=k;i++){
    				int anc = f[tx][i-1];
    				f[tx][i] = f[anc][i-1];
    			}
    		}
    	}
    }
    
    void dfs(int x, int fa)
    {
    	for(int tx : e[x])
    	{
    		if(tx == fa) continue;
    		dfs(tx, x);
    		b[x] += b[tx];
    	}
    	
    	if(x == 1) return;
    	if(!b[x]) ans += m;
    	else if(b[x] == 1) ans ++;
    }
    
    int lca(int x, int y)
    {
    	if(dep[x] < dep[y]) swap(x, y);
    	
    	for(int i=k;i>=0;i--)
    	{
    		if(dep[f[x][i]] >= dep[y]) x = f[x][i];
    	}
    	
    	if(x == y) return x;
    	
    	for(int i=k;i>=0;i--)
    	{
    		if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    	}
    	return f[x][0];
    }
    
    signed main(){
    	Ios;
    	cin >> n >> m;
    	k = log(n) / log(2);
    	
    	for(int i=1;i<n;i++){
    		int x, y;cin >> x >> y;
    		e[x].push_back(y);
    		e[y].push_back(x);
    	}
    	
    	bfs();
    	
    	for(int i=1;i<=m;i++)
    	{
    		int x, y; cin >> x >> y;
    		b[x] ++, b[y]++;
    		b[lca(x, y)] -= 2;
    	}
    	
    	dfs(1, 0);
    	
    	cout << ans;
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    点差分:Eezie and Pie

    题意
    给定一棵 n 个节点的树,每一个节点有属性 d i s i dis_i disi
    对于一个节点 x 来说,其能捕获到其子树的所有节点 tx 中,所有到节点 x 的距离不超过自身属性 d i s t x dis_{tx} distx 的节点。
    求出每个节点能够捕获的节点数量。

    1 ≤ N ≤ 2 × 1 0 6 ,   0 ≤ d i ≤ N 1≤N≤2×10^6,\ 0≤d_i≤N 1N2×106, 0diN

    思路
    正着来想,对于一个节点来说,要遍历其子树中的所有节点,判断每个节点是否能够被自己捕获。如果每次都遍历的话,时间复杂度太高。然后想,从下到上回溯的过程中,把当前节点放到 vector 中,存两个权值,一个是属性,一个是深度,往上走的过程中,把深度差大于其属性的节点从中删掉。
    虽然往上走的过程中深度差在不断加大,但还有一个属性限制,所以旧加入的点不一定先清理掉,所以不能像双端队列那样,不满足的都集中在后面每次判断后面就行。所以,这里的清除只能遍历所有节点,复杂度就又上升了。

    当正着走的思路行不通时,及时止损,换一种思路!

    反过来想!
    考虑每个节点对上面节点的贡献,看这个节点会被上面的哪些节点所捕获。
    很明显,每个节点 x 会对其上面的 d i s i dis_i disi 个节点有贡献,那么就将上面的这 d i s i dis_i disi 个节点的答案 +1。
    将树上的一段链上点的权值都 +1,用到树上差分。

    这里是点差分,将该链两端点的点权都 +1,将其 lca 的点权 -1,将 lca 父节点的点权 -1。求和恢复原值,以节点 x 为根的子树点权之和便是更新后节点 x 的点权。

    在这题里是一条竖着链,所以不用求 lca,将最顶端节点的父节点点权 -1,最底端节点点权 +1 即可。

    但还有一个问题,如何找到链最顶端的点?
    能够确定链最顶端点的深度 dep,这个深度可能有多个点,我们要的只是从根节点到节点 x 这条路径上,深度为 dep 的点。为了找到这个点,还需要用倍增。
    对于每个节点,维护 f[i, j] 表示从该节点出发,往上跳 2 j 2^j 2j 步到达的节点。
    然后只要深度大于等于 dep 就跳,一直跳到深度 dep 或者 根节点,复杂度 logm,m为树深。
    注意特判不要跳出根节点。

    #include
    using namespace std;
    
    #define Ios ios::sync_with_stdio(false),cin.tie(0)
    #define int long long
    
    const int N = 2000010, mod = 1e9+7;
    int T, n, m;
    int a[N];
    int dep[N], f[N][30];
    vector<int> e[N];
    int k, sum[N], d[N];
    
    void bfs()
    {
    	queue<int> que;
    	que.push(1);
    	dep[0] = 0, dep[1] = 1;
    	
    	while(que.size())
    	{
    		int x = que.front(); que.pop();
    		
    		for(int tx : e[x])
    		{
    			if(tx == f[x][0]) continue;
    		
    			dep[tx] = dep[x] + 1;
    			f[tx][0] = x;
    			que.push(tx);
    			for(int i=1;i<=k;i++)
    				f[tx][i] = f[f[tx][i-1]][i-1];
    		}
    	}
    }
    
    void dfs(int x, int fa)
    {
    	sum[x] = d[x];
    	
    	for(int tx : e[x])
    	{
    		if(tx == fa) continue;
    		if(!sum[tx]) dfs(tx, x);
    		sum[x] += sum[tx];
    	}
    }
    
    signed main(){
    	scanf("%lld", &n);
    	
    	k = log(n)/log(2);
    	
    	for(int i=1;i<n;i++)
    	{
    		int x, y; scanf("%lld%lld", &x, &y);
    		e[x].push_back(y);
    		e[y].push_back(x);
    	}
    	for(int i=1;i<=n;i++) scanf("%lld", &a[i]);
    	
    	bfs();
    	
    	for(int i=1;i<=n;i++)
    	{
    		int x = i;
    		for(int j=k;j>=0;j--)
    		{
    			if(dep[i] - dep[f[x][j]] <= a[i] && f[x][j] > 0) x = f[x][j];
    		}
    		d[i] ++, d[f[x][0]]--;
    	}
    	
    	dfs(1, 0);
    	
    	for(int i=1;i<=n;i++) printf("%lld ", sum[i]);
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    经验

    • 树上边差分,将树上一条链上所有边的权值都 +c。
      把每条边都附着在其下端点上。
      将链两端点的点权分别 +c,将其 lca 的点权 -2c。
      最后求和恢复原值,以节点 x 为根的子树点权之和便是节点 x 上面那条边的新边权。
    • 树上点差分,将树上一条链上所有点的权值都 +c。
      将链两端点的点权分别 +c,将其 lca 的点权 -c,lca 父节点的点权 -c。
      最后求和恢复原值,以节点 x 为根的子树点权之和便是节点 x 的新点权。
    • 倍增找点,logn 找到从根节点到当前节点路径中,满足条件的节点。
  • 相关阅读:
    【springboot+vue项目学习】Unable to start web server引发的问题串
    性能测试:LoadRunner中事务和集合点的前后顺序
    QML android 采集手机传感器数据 并通过udp 发送
    深入了解MySQL内部细节
    使用Python的Flask框架开发验证码登录功能
    《QA离业务代码能有多近?》轻量级单元测试方案
    基于WebGL、Cesium技术的三维空间可视化
    Java运算符及流程控制
    Python机器学习bug:ValueError_ Expected 2D array, got 1D array instead
    单片机矩阵键盘
  • 原文地址:https://blog.csdn.net/Mr_dimple/article/details/126201717