YbtOJ 「动态规划」 第4章 树形DP

树形dp

这类题一般在图的遍历过程中dp

void dfs ( int u , int fa )
{
	//dp初始化
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == fa ) continue;
		dfs ( v , u );
		//合并的时候统计信息
	}
}

A. 【例题1】树上求和

luogu原题 P1352 没有上司的舞会

[题目描述]

某大学有 n 个职员,编号为 1n

他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。

现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。

所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

[输入格式]

输入的第一行是一个整数 n

2 到第 (n+1) 行,每行一个整数,第 (i+1) 行的整数表示 i 号职员的快乐指数 ri

(n+2) 到第 2n 行,每行输入一对整数 l,k,代表 kl 的直接上司。

[输出格式]

输出一行一个整数代表最大的快乐指数。

[算法分析]

考虑设dp[i][0]表示当前进行到了i号节点 这个i号节点不选 子树的最大值

反之 dp[i][1]表示当前进行到了i号节点 这个i号节点选 子树的最大值

[代码实现]


#include <bits/stdc++.h>
using namespace std;
#define endl '\n'

#define inl inline

const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int head[N] , cnt;
struct node { int to , nxt; } e[N];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

int n , a[N] , f[N][2] , in[N] , rt;

void dfs ( int u , int ff )
{
	f[u][1] = a[u] , f[u][0] = 0;
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue;
		dfs ( v , u );
		f[u][1] += f[v][0];
		f[u][0] += max ( f[v][1] , f[v][0] );
	}
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
	for ( int i = 1 , u , ff ; i < n ; i ++ ) u = read() , ff = read() , add ( ff , u ) , in[u] = 1;
	for ( int i = 1 ; i <= n ; i ++ ) if ( !in[i] ) rt = i;
	dfs ( rt , 0 );
	cout << max ( f[rt][1] , f[rt][0] ) << endl;
	return 0;
}


B. 【例题2】结点覆盖

[题目描述]

五一来临,某地下超市为了便于疏通和指挥密集的人员和车辆,以免造成超市内的混乱和拥挤,准备临时从外单位调用部分保安来维持交通秩序。

已知整个地下超市的所有通道呈一棵树的形状;某些通道之间可以互相望见。总经理要求所有通道的每个端点(树的顶点)都要有人全天候看守,在不同的通道端点安排保安所需的费用不同。

一个保安一旦站在某个通道的其中一个端点,那么他除了能看守住他所站的那个端点,也能看到这个通道的另一个端点,所以一个保安可能同时能看守住多个端点(树的结点),因此没有必要在每个通道的端点都安排保安。

编程任务:

请你帮助超市经理策划安排,在能看守全部通道端点的前提下,使得花费的经费最少。

[输入格式]

第1行 n,表示树中结点的数目。

第2行至第n+1行,每行描述每个通道端点的信息,依次为:该结点标号i0<in,在该结点安置保安所需的经费(k<=10000,该边的儿子数m,接下来m个数,分别是这个节点的m个儿子的标号r1r2,...,rm

对于一个n0<n<=1500)个结点的树,结点标号在1到n之间,且标号不重复。

[输出格式]

最少的经费。

如右图的输入数据示例

输出数据示例:

[代码实现]

f[i][j]表示以i为根节点的子树全部被覆盖,且i的覆盖方式为k的最小花费

f[u][0]u的父亲结点,f[u][1] 表示选$u f[u][2]u$的儿子.

同树形dp 讨论三种情况(如果不是第二种情况这个点就一定不选 这是最关键的地方)

  1. 如果这个点被父亲覆盖 那么子节点不可能被父亲覆盖 而是在其余两种情况当中选取
  2. 如果这个点被自己覆盖 那么子节点三种情况都行 ( 因为子节点肯定已经被覆盖了 那么选哪种情况都可以了)
  3. 如果这个点被儿子覆盖 那么子节点必须被自己覆盖 那么再分两种情况:
    • 如果f[v][1]<f[v][2] 那么皆大欢喜 强制让子节点选取自己 f[u][2]+=f[v][1] 同时标记flag=1
    • 如果反之 则每一个点累加f[u][2]+=f[v][2] 并处理子节点选择自身和选择儿子的差值的最小值minn=min(minn,f[v][1]f[v][2])作为损失
    • 如果所有节点都遍历过了还是没有找到f[v][1]<f[v][2]的情况(flag=0) 那么f[u][2]+=minn作为损失(此时的方案选取情况就是:强制让这一个minn差值所对应的子节点vi改变选择 让自己覆盖自己 其余的出点v还是由自己的儿子覆盖 这样不会造成其他节点被错误覆盖 因为你所有子节点的f[v][0/1/2]都被处理过了)
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e6 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , a[N] , f[N][3] , in[N] , rt;

int head[N] , cnt;
struct node { int to , nxt; } e[N];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }


void dfs ( int u , int ff )
{
	int minn = inf , flag = 0;
	f[u][1] = a[u];
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue;
		dfs ( v , u );
		f[u][0] += min ( f[v][1] , f[v][2] );
		f[u][1] += min ( f[v][0] , min ( f[v][1] , f[v][2] ) );
		if ( f[v][1] <= f[v][2] ) flag = 1 , f[u][2] += f[v][1];
		else f[u][2] += f[v][2] , minn = min ( minn , f[v][1] - f[v][2] );
	}
	if ( !flag ) f[u][2] += minn;
}

signed main ()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
//	memset ( f , inf , sizeof f );
	n = read();
	for ( int i = 1 , u , num , v ; i <= n ; i ++ )
	{
		u = read() , a[u] = read() , num = read();
		for ( int j = 1 ; j <= num ; j ++ ) v = read() , add ( u , v ) , in[v] = 1;
	}
	for ( int i = 1 ; i <= n ; i ++ ) if ( !in[i] ) rt = i;
	dfs ( rt , 0 );
	cout << min ( f[rt][1] , f[rt][2] ) << endl;	
	return 0;
}

C. 【例题3】最长距离

树的直径模板题 一个结论:在树上 到一个点距离最远的点一定是这棵树的直径的两个端点之一

所以我们先dfs搜出距离任意一点的最远点 再从该点开始搜索到直径的另一个端点 同时更新dis1[i]数组(表示直径的这个端点到各个点的距离)

之后再从这次搜出来的最远点(直径的另一个端点)搜索 更新dis2[i]数组(表示直径的这个端点到各个点的距离)

所以对于每一个点的最长路径就是max(dis1[i],dis2[i])

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , s , dis1[N] , dis2[N] , maxlen;

int head[N] , cnt;
struct node { int to , nxt , w; } e[N];
inl void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }

void dfs1 ( int u , int ff )
{
	if ( dis1[u] > maxlen ) s = u , maxlen = dis1[u];
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue; 
		dis1[v] = dis1[u] + e[i].w;
		dfs1 ( v , u );
	}
}

void dfs2 ( int u , int ff )
{
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue; 
		dis2[v] = dis2[u] + e[i].w;
		dfs2 ( v , u );
	}
}

void init()
{
	memset ( dis1 , 0 , sizeof dis1 );
	memset ( dis2 , 0 , sizeof dis2 );
	memset ( head , 0 , sizeof head );
	maxlen = 0 , cnt = 0;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	while ( cin >> n )
	{
		init();
		for ( int i = 2 , u , w ; i <= n ; i ++ ) u = read() , w = read() , add ( i , u , w ) , add ( u , i , w );
		dfs1 ( 1 , 0 ) , memset ( dis1 , 0 , sizeof dis1 );
		maxlen = 0 , dfs1 ( s , 0 );
		maxlen = 0 , dfs2 ( s , 0 );
		for ( int i = 1 ; i <= n ; i ++ ) cout << max ( dis1[i] , dis2[i] ) << endl;
	}
	return 0;
}

D. 【例题4】选课方案

[题目描述]

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 M 门课程学习,问他能获得的最大学分是多少?

[输入格式]

第一行有两个整数 N , M 用空格隔开。( 1N300 , 1M300 )

接下来的 N 行,第 I+1 行包含两个整数 kisi, ki 表示第I门课的直接先修课,si 表示第I门课的学分。若 ki=0 表示没有直接先修课(1kiN , 1si20)。

[输出格式]

只有一行,选 M 门课程的最大得分。

[算法分析]

树上背包经典例题

得到dp[u][j]表示以节点u的子树中,选择j门课的最大权值和(价值和)

状态转移:dp[u][j]=max(dp[u][j],dp[v][jk]+dp[u][k])

这道题是多叉树 但是可以在dp到v子节点的时候 将v树看成一个子树 将其他叉全都看成另一个子树即可

注意对于u节点 必须有一个节点来选取 所以枚举其他子树的权值dp[u][k]的时候需要从1开始到k

[代码实现]

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e3 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , f[N][N] , m;

int head[N] , cnt;
struct node { int to , nxt; } e[N];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

void dfs ( int u )
{
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		dfs(v);
		for ( int j = m ; j ; j -- )
			for ( int k = 1 ; k <= j ; k ++ )//不能枚举到k 因为我们状态定义的就是u这个根节点必须选 因为只有选了这个节点才能选子节点
				f[u][j] = max ( f[u][j] , f[v][j-k] + f[u][k] );
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 ; i <= n ; i ++ ) 
	{
		int u = read();
		f[i][1] = read();
		add ( u , i );
	}
	m ++ , dfs(0);
	cout << f[0][m] << endl;
	return 0;
}

E. 1.路径求和

我们考虑计算每条边对答案的贡献 对于每一条边 它的计算次数等于它分开的两棵子树中 左叶数×右总节点数+右叶数×右总结点数 这两个都可以通过dfs维护

也就是lleaf×rsize+lsize×rleaf

所以贡献再乘上e[i].w即可

注意:题目中读入顺序是先边权后节点

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define int long long
#define inl inline
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , s , leaf[N] , sz[N] , out[N] , ans;

int head[N] , cnt;
struct node { int to , nxt , w; } e[N<<1];
inl void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }

void dfs1 ( int u , int ff )
{
	sz[u] = 1;
	if ( out[u] == 1 ) leaf[u] = 1;//叶子节点
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue; 
		dfs1 ( v , u );
		sz[u] += sz[v];
		leaf[u] += leaf[v];
	}
}

void dfs2 ( int u , int ff )
{
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue;
		dfs2 ( v , u );
		ans += e[i].w * ( sz[v] * ( leaf[1] - leaf[v] ) + ( sz[1] - sz[v] )  * leaf[v] );
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 , u , v , w ; i <= m ; i ++ ) w = read() , u = read() , v = read() , add ( v , u , w ) , add ( u , v , w ) , out[u] ++ , out[v] ++;
	dfs1 ( 1 , 0 );
	dfs2 ( 1 , 0 );
	cout << ans << endl;
	return 0;
}

F. 2.树上移动

对于第一个问题 答案就是所有边权之和乘二减去过起点s的最长路径

第二问即为求树的直径长度 用所有边权乘二减去直径长度即可

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
const int N = 1e5 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , s , sum , dis1[N] , dis2[N] , maxlen , maxx;

int head[N] , cnt;
struct node { int to , nxt , w; } e[N<<1];
inl void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }

void dfs1 ( int u , int ff )
{
	if ( dis1[u] > maxlen ) s = u , maxlen = dis1[u];
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue; 
		dis1[v] = dis1[u] + e[i].w;
		dfs1 ( v , u );
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , s = read();
	for ( int i = 1 , u , v , w ; i < n ; i ++ )
		u = read() , v = read() , w = read() , add ( v , u , w ) , add ( u , v , w ) , sum += 2 * w;
	dfs1 ( s , 0 );
	cout << sum - maxlen << endl;
	memset ( dis1 , 0 , sizeof dis1 );
	maxlen = 0 , dfs1 ( s , 0 );
	cout << sum - maxlen << endl;
	return 0;
}

G. 3.块的计数

连通块:无向图

强连通:有向图

我们考虑dp 对于节点u 定义ans[u] 表示以节点u为根的包含最大喜好值的连通块个数(以u为根则必须算u)发现如果从u的孩子转移的话需要讨论很多情况的

我们利用补集转化思想 f[u]表示以节点u为根的连通块总个数 g[u]表示以节点u为根的不包含最大喜好值的连通块个数 那么就是ans[u]=f[u]g[u]

先考虑如何求f[u] f[u]=f[v]+1 (子节点的连通块方案数总和+这个子节点不选的方案数)

然后 g[u]=g[v]+1(同上子节点的方案数总和+这个子节点不选的方案数)

注意:还有一种情况 如果u节点本身是最大值点 那么它的子树中不存在以u为根的不包含最大喜好值的连通块 那么这个g[u]的值初始时就是0 乘多少也没用 符合条件

最后统计答案就是i=1nf[i]g[i]

需要注意求maxx的时候初始值需要赋为inf

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
#define int long long 
const int N = 2e5 + 5;
const int mod = 998244353;
const int inf = 2e18;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , maxx = -inf , a[N] , totg , totf , f[N] , g[N];
//f[i]表示以节点u为根的联通块总个数,g[i]表示以节点u为根且不包含这个最大喜好值的联通块个数 

int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

void dfs  ( int u , int ff )
{
	if ( a[u] != maxx ) g[u] = 1;
	f[u] = 1;
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue;
		dfs ( v , u );
		f[u] = f[u] * ( f[v] + 1 ) % mod;//f[v]的方案数+不选的方案
		g[u] = g[u] * ( g[v] + 1 ) % mod;//g[v]的方案数+不选的方案
	}
	totf = ( totf + f[u] ) % mod;
	totg = ( totg + g[u] ) % mod;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read();
	for ( int i = 1 ; i <= n ; i ++ ) a[i] = read() , maxx = max ( maxx , a[i] );
	for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
	dfs ( 1 , 0 );
	cout << ( totf + mod - totg  ) % mod;
	return 0;
}

H. 4.树的合并

新生成的树上的直径只可能在以下三种情况中选取:

  1. 第一棵树的直径
  2. 第二颗树的直径
  3. 新生成的最长链(连的两个点在两棵树中延伸的最长距离+1)

那么我们首先求出左右两棵树的直径的最大值(记为maxx)和每一个点到这棵树中其他节点的最远距离 记为f1,f2 这样 对于每一个点对(i,j) 它对答案的贡献就是max(f1[i]+f2[j]+1,maxx)

那么我们对于f1先进行排序(从大到小)并求出前缀和 然后对于每一个f2f1中二分一个小于等于maxxf21的位置

计入答案为ans+=sum[pos]+(f2[i]+1)pos+(npos)maxx;

即为前面f1[i]+f2[j]+1>maxx的所有点对的路径权值之和 加上小于maxx的所有点对的路径权值之和

注意long long

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define inl inline
#define int long long 
const int N = 2e5 + 5;
const int inf = 0x3f3f3f3f;

int read ()
{
	int x = 0 , f = 1;
	char ch = cin.get();
	while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
	while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
	return x * f;
}

int n , m , s , dis1[N] , dis2[N] , f1[N] , f2[N] , maxlen , maxx , sum[N] , ans;

int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
inl void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

void init()
{
	memset ( dis1 , 0 , sizeof dis1 );
	memset ( dis2 , 0 , sizeof dis2 );
	maxlen = 0 , s = 0;
}

void dfs1  ( int u , int ff )
{
	if ( dis1[u] > maxlen ) maxlen = dis1[u] , s = u;
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue;
		dis1[v] = dis1[u] + 1; 
		dfs1 ( v , u );
	}
}

void dfs2  ( int u , int ff )
{
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == ff ) continue;
		dis2[v] = dis2[u] + 1; 
		dfs2 ( v , u );
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	n = read() , m = read();
	for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
	dfs1 ( 1 , 0 );
	memset ( dis1 , 0 , sizeof dis1 );
	maxlen = 0 , dfs1 ( s , 0 );
	maxx = maxlen;//记录左面直径长度
	maxlen = 0 , dfs2 ( s , 0 );
	for ( int i = 1 ; i <= n ; i ++ ) f1[i] = max ( dis1[i] , dis2[i] );
	
	init();
	for ( int i = 1 , u , v ; i < m ; i ++ ) u = read() , v = read() , add ( u + n , v + n ) , add ( v + n , u + n );
	dfs1 ( n + 1 , 0 );
	memset ( dis1 , 0 , sizeof dis1 );
	maxlen = 0 , dfs1 ( s , 0 );
	maxx = max ( maxx , maxlen );//记录左面直径长度
	maxlen = 0 , dfs2 ( s , 0 );
	for ( int i = n + 1 ; i <= n + m ; i ++ ) f2[i] = max ( dis1[i] , dis2[i] );//注意m2中的所有处理都是从n+1开始的
	sort ( f1 + 1 , f1 + n + 1 , [](const int a , const int b) { return a > b; } );
	
	for ( int i = 1 ; i <= n ; i ++ ) sum[i] = sum[i-1] + f1[i];
	for ( int i = n + 1 ; i <= n + m ; i ++ )
	{
		int pos = lower_bound ( f1 + 1 , f1 + n + 1 , maxx - f2[i] - 1 , greater<int>() ) - f1 - 1;//大于maxx-f2[i]的最后一个
		ans += sum[pos]	+ ( f2[i] + 1 ) * pos + ( n - pos ) * maxx;
	}
	cout << ans << endl;
	return 0;
}


I. 5.权值统计

一道神题 先放代码 留坑待填

我们用f[u]表示以u为根的答案(然后就不会了)

#include <bits/stdc++.h>
using namespace std;
#define int long long

const int N = 1e5 + 10;
const int mod = 10086;
int s , n , tot , a[N] , ans , maxx;
int f[N];
//g[i]表示以节点u为根的联通块总个数,f[i]表示以节点u为根且不包含这个最大喜好值的联通块个数 
int head[N] , cnt;
//f[u]表示u子树的答案
struct edge
{
	int to , nxt;
}e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] }; head[u] = cnt; }

void dfs ( int u , int fa )
{
	int s1 = 0 , s2 = 0 , s3 = 0;
	for ( int i = head[u] ; i ; i = e[i].nxt )
	{
		int v = e[i].to;
		if ( v == fa ) continue;
		dfs ( v , u );
		s1 += f[v];//子树答案之和 
		s2 += f[v] * f[v]; 
	}
	f[u] = ( s1 + 1 ) * a[u] % mod;
	s3 = ( ( s1 * s1 - s2 ) >> 1 ) % mod;//s3是子树答案和 
	ans = ( ans + f[u] + s3 * a[u] ) % mod;
}


signed main()
{
	scanf ( "%lld" , &n );
	for ( int i = 1 ; i <= n ; i ++ ) scanf ( "%lld" , &a[i] ) , maxx = max ( maxx , a[i] );
	for ( int i = 1 , u , v ; i < n ; i ++ ) scanf ( "%lld%lld" , &u , &v ) , add ( u , v ) , add ( v , u );
	dfs ( 1 , 0 );
	printf ( "%lld" , ans );
}

posted @   Echo_Long  阅读(155)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示