牛客2021年度训练联盟热身训练赛第一场(讲题)

E题: Early Orders

题意:给定 n ,k 和长度为n的数组x[i]

1<=k<=n<=2e5, 1<=x[i]<=k;

要在这个数组里找一个长度为k的子序列,使得数字1到k在这个序列里各出现一次,输出字典序最小的子序列

输入保证数组 x[i] 包括了1到k的所有数字

 

思路:很容易想到贪心地选择小的数放在前面,可以用单调栈来做

做法:

 

从左往右扫原数组,对于x[i]:

1、栈为空时,数字入栈;

2、栈非空,那么可以把栈里大于x[i]的数弹出,(从栈顶依次往下弹出) ,这样就维护了一个单调栈;

3、栈里存在数字x[i],直接continue,就算栈里有比x[i]大的数也不弹出;(可以试试1 2 1 3 2这个样例,1不能把2弹出,否则1 2 3变成1 3 2)

4、栈里的某个数字在x[i+1]到x[n]都不存在,那么这个数就算比 x[i] 大,也不弹出这个数,就是说从栈顶弹出到这种数就不继续弹了

注释代码:

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int MAXN = 2e5 + 7;
int a[MAXN];
int last[MAXN];
bool vis[MAXN];
int ans[MAXN];
int st[MAXN];//手写栈
int main()
{
	int n, k;
	cin >> n >> k;
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
	int l = 0, r = 0;
	for (int i = 1; i <= k; i++) vis[i] = false;
	for (int i = 1; i <= n; i++) last[i] = false;
	for (int i = n; i; i--) {
		if (!vis[a[i]]) {
			vis[a[i]] = true;
			last[a[i]] = i;//数字a[i]在数组里最后一次出现的位置是last[a[i]]
		}
	}
	for (int i = 1; i <= k; i++) vis[i] = false;
	for (int i = 1; i <= n; i++) {
		if (vis[a[i]]) continue;//栈里有a[i]这个数字了,直接continue
		while (r != l && a[i] <= st[r] && i <= last[st[r]]) {//栈顶数字比a[i]大,同时还没有扫到栈顶数字最后一次出现的位置,就弹出栈顶
			vis[st[r]] = false;//栈顶数字被弹出,栈里面就没有这个数字了,把vis置为false
			r--;//弹出操作
		}
		st[++r] = a[i];//入栈操作
		vis[a[i]] = true;//在栈里面,vis置为true
	}
	printf("%d", st[1]);
	for (int i = 2; i <= k; i++) printf(" %d", st[i]);
	printf("\n");
	return 0;
}

  

如果觉的自己的代码没问题,但就是wa了,欢迎找我帮忙造反例

 

 

I题:Full Depth Morning Show

题意:给定一颗节点数为n的树,给定每个节点的点权 t[ i ] ,给定 n - 1条边(u,v,w),w为这条边的边权,用 f[u][v]表示从u到v的这条路径的花费

 

题目定义f[u][v] 为u到v的路径中所有边的边权之和乘以u,v的点权之和,用公式来表示就是f[u][v] = (Σw)* (t[ u ] + t[ v ])

 

总花费为所有从 非根节点 到 根节点 的路径的花费之和,即计算Σf[u][root] (u!=root)

在第一行输出以一号节点为根节点时,总共的花费

在第二行输出以二号节点为根节点时,总共的花费

 

。。。

一共输出n行

思路:

所有节点都要体验一遍当根节点的情况,那么可以考虑用换根dp,记dp[i]为以 i 节点为根节点的总贡献

怎么计算总花费,我们可以换个角度来计算,不把它看成是所有路径的贡献之和,而看成是所有边的贡献之和

考虑以一号节点为根节点,任选一条边,它的贡献怎么算,用 g[i] 表示一条边的贡献

如图:

 

 这样我们得到了任意一条边的贡献为:g[ i ] = (t_sum[子树] + size[子树] * t[root] ) * w[ i ]

把所有边的贡献加起来就是总贡献:dp[root] = Σg[i]

我们一开始可以把这棵树以1号节点为根节点,先扫一遍,计算出分别以各个节点为根节点的子树,它的大小size[子树],它的t_sum[子树]

然后再扫一遍,计算出以1号节点为根节点时,用计算出所有边的贡献并加起来的方法计算出总贡献

这两次扫描可以一起扫描

代码:

void dfs1(int s, int f) {
	siz[s] = 1; t_sum[s] += t[s];
	for (int i = head[s]; i; i = edge[i].next) {//这段是求每个节点的siz[]和t_sum[]
		int po = edge[i].to;
		if (po == f) continue;
		fa[po] = s;
		dfs1(po, s);
		siz[s] += siz[po];
		t_sum[s] += t_sum[po];
	}
	for (int i = head[s]; i; i = edge[i].next) {//这段是计算每条边的贡献并加起来
		int po = edge[i].to;
		if (po == fa[s]) continue;
		long long w = edge[i].cost;
		dp[1] += t_sum[po] * w;
		su += (long long)siz[po] * w;
	}
}

  

dp[1] += su * t[1];

  

这样我们得到了dp[1],现在考虑dp数组怎么转移

dp[1] = Σg[i] = Σ(  (t_sum[子树] + size[子树] * t[1] ) * w[ i ]  )

设2号节点为1号节点的儿子节点

现把根节点从1号节点转移到2号节点

如图:

只有两个节点的t_sum[子树]和size[子树]改变了,同时对比以下这两项

dp[1] = Σg[i] = Σ(  (t_sum[子树] + size[子树] * t[1] ) * w[ i ]  )

dp[2] = Σg[i] = Σ(  (t_sum[子树] + size[子树] * t[2] ) * w[ i ]  )

 

 

由于只有两个节点的t_sum[子树]发生了变化,而所有size[子树]乘的t[1]变成了t[2]

所以我们把边的贡献拆成两项g[i] = t_sum[子树] * w[i] (第一项) + size[子树]*t[2]*w[i](第二项)

 

可以发现,只有一条边的贡献的第一项发生了变化,而所有边的贡献的第二项发生了变化,但是第二项里的size[子树]*w[i]也是只有一条边发生了变化

如图:

所以我们要分别来计算这两项的改变量

计算第一项的改变量,我们可以把改变了的那条边的第一项的改变量求出来

计算第二项的改变量,我们可以用su[i]表示以 i 为根节点时, Σsize[子树]*w[i]为多少: su[i] = Σsize[子树]*w[i] 。然后就好算了

 

能算出这两项改变量,就可以用dfs扫一遍来在树上算dp[i]了

 

dfs代码:

void dfs2(int s) {
	for (int i = head[s]; i; i = edge[i].next) {
		int po = edge[i].to;
		if (po == fa[s]) continue;
		long long w = edge[i].cost;
		dp[po] = dp[s] - (t_sum[po] * w) - su[s] * t[s];
		dp[po] += (t_sum[1] - t_sum[po]) * w;
		su[po] = su[s] - (long long)siz[po] * w;
		su[po] += ((long long)siz[1] - siz[po]) * w;
		dp[po] += su[po] * t[po];
		dfs2(po);
	}
}

  

总的代码:

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int MAXN = 2e5 + 7;
struct EDGE {
	int to, next;
	long long cost;
}edge[MAXN*2];
int head[MAXN], tot = 0;
void add(int u, int v, long long w) {
	tot++;
	edge[tot].to = v;
	edge[tot].cost = w;
	edge[tot].next = head[u];
	head[u] = tot;
}
long long dp[MAXN],t[MAXN],t_sum[MAXN],su[MAXN];
int siz[MAXN], fa[MAXN];
void dfs1(int s, int f) {
	siz[s] = 1; t_sum[s] += t[s];
	for (int i = head[s]; i; i = edge[i].next) {//这段是求每个节点的siz[]和t_sum[]
		int po = edge[i].to;
		if (po == f) continue;
		fa[po] = s;
		dfs1(po, s);
		siz[s] += siz[po];
		t_sum[s] += t_sum[po];
	}
	for (int i = head[s]; i; i = edge[i].next) {//这段是计算每条边的贡献并加起来
		int po = edge[i].to;
		if (po == fa[s]) continue;
		long long w = edge[i].cost;
		dp[1] += t_sum[po] * w;
		su[1] += (long long)siz[po] * w;
	}
}
void dfs2(int s) {
	for (int i = head[s]; i; i = edge[i].next) {
		int po = edge[i].to;
		if (po == fa[s]) continue;
		long long w = edge[i].cost;
		dp[po] = dp[s] - (t_sum[po] * w) - su[s] * t[s];
		dp[po] += (t_sum[1] - t_sum[po]) * w;
		su[po] = su[s] - (long long)siz[po] * w;
		su[po] += ((long long)siz[1] - siz[po]) * w;
		dp[po] += su[po] * t[po];
		dfs2(po);
	}
}
int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; i++) {
		scanf("%lld", &t[i]);
	}
	int u, v; long long w;
	for (int i = 1; i <= n - 1; i++) {
		scanf("%d%d%lld", &u, &v, &w);
		add(u, v, w);
		add(v, u, w);
	}
	dfs1(1, 0);
	dp[1] += su[1] * t[1];
	dfs2(1);
	for (int i = 1; i <= n; i++) {
		printf("%lld\n", dp[i]);
	}
	return 0;
}

  

有许多树上的题都是这种二次dfs扫描,考虑换根后的改变量

posted @ 2021-03-07 22:10  beta_dust  阅读(206)  评论(0编辑  收藏  举报