[省选集训2022] 模拟赛1

中心城镇问题

题目描述

给出一个 \(n\) 个点的树,第 \(i\) 个点的权值是 \(a_i\),现在要选择一些点建立据点,要求任意两个据点之间的距离必须大于 \(k\),问选出据点的最大权值和是多少。

\(n\leq 10^6\)

解法

我发现我学不懂长链剖分的原因是指针基础为​零,而今天终于把我的心头大患解决了

其实这题根本不需要任何题意转化,我们把两个据点的限制在 \(\tt lca\) 处考虑即可,那么可以直接掏出树形 \(dp\),设 \(dp[u][i]\) 表示点 \(u\) 内最浅的据点离 \(u\) 的距离是 \(i\),选出的最大权值和。转移考虑合并两棵子树,考虑最浅的点是再原来的子树中还是在新的子树中,我们枚举最浅点的距离 \(j(j\leq dep[v])\),设 \(o=\max(j,k-j+1)\),表示选取其他点的最小深度,这要求我们再求出 \(tmp[u][i]\) 表示 \(dp\) 数组的后缀最大值:

\[dp[u][j]\leftarrow \max(dp[v][j-1]+tmp[u][o],tmp[v][o-1]+dp[u][j]) \]

然后简单讲一下长链剖分需要用到的指针知识:*p 作为一个指针可以用 p=a 操作来指向数组的第一个元素,然后直接用 p[x] 就相当于取 *(p+x),所以我们的 \(dp\) 数组就设置成一个指针数组即可,再根据深度设置指针的初始位置。

实现细节:\(dep[u]\) 代表子树 \(u\) 的长链长度,那么 \(dp[u]\) 范围是 \([0,dep[u])\),转移时要注意卡好范围。

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1000005;
#define int long long
const int inf = 1e18;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,k,tot,f[M],a[M],son[M],dep[M],qwq[M<<1];
int *dp[M],*tmp[M];
struct edge {int v,next;}e[M<<1];
void upd(int &x,int y) {x=max(x,y);}
void pre(int u,int fa)
{
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(v==fa) continue;
		pre(v,u);
		if(dep[v]>dep[son[u]]) son[u]=v;
	}
	dep[u]=dep[son[u]]+1;
}
void dfs(int u,int fa)
{
	if(son[u])
	{
		dp[son[u]]=dp[u]+1;
		tmp[son[u]]=tmp[u]+1;
		dfs(son[u],u);
	}
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(v==fa || v==son[u]) continue;
		dp[v]=dp[u]+dep[u];
		tmp[v]=tmp[u]+dep[u];
		dfs(v,u);
		for(int j=dep[v];j;j--)
		{
			int o=max(j,k-j+1),t=-inf;
			//the min-dis lies in v
			if(o<dep[u]) upd(t,tmp[u][o]+dp[v][j-1]);
			else upd(t,dp[v][j-1]);//no assistance
			//the min-dis
			if(o<=dep[v]) upd(t,tmp[v][o-1]+dp[u][j]);
			else upd(t,dp[u][j]);//no assistance
			dp[u][j]=t;
		}
		for(int j=dep[v];j;j--)
		{
			tmp[u][j]=dp[u][j];
			if(j<dep[u]-1) upd(tmp[u][j],tmp[u][j+1]);
		}
	}
	dp[u][0]=a[u];
	if(k+1<dep[u]) dp[u][0]+=tmp[u][k+1];
	tmp[u][0]=dp[u][0];
	if(dep[u]>1) upd(tmp[u][0],tmp[u][1]);
}
signed main()
{
	freopen("central.in","r",stdin);
	freopen("central.out","w",stdout); 
	n=read();k=read();
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		e[++tot]=edge{v,f[u]},f[u]=tot;
		e[++tot]=edge{u,f[v]},f[v]=tot;
	}
	dp[1]=qwq;tmp[1]=qwq+n+1;
	pre(1,0);dfs(1,0);
	printf("%lld\n",tmp[1][0]);
}

心理阴影

题目描述

给定一棵 \(n\) 个节点的有根树,现在有一个排列,满足若 \(u\) 是 \(v\) 的父节点则 \(u\) 在 \(v\) 之前,问这个排列逆序对的期望。

\(n\leq 4000\)

解法

有一个 \(O(n^5)\) 的算法可以拿到 \(50\) 分,就是你枚举两个点,然后用组合数加 \(dp\) 算贡献即可。

正解却是直接 \(dp\) 算贡献,考虑到逆序对是一个二元关系,那么我们同样也去设计二元关系的状态,设 \(dp[u][v]\) 表示把子树 \(u,v\) 合并之后 \(u,v\) 之间产生的期望逆序对数,转移考虑第一个位置是 \(u\) 还是 \(v\):

\[f(u,v)=\frac{siz_u}{siz_u+siz_v}\cdot (\sum_{x\in son_u} f(x,v)+g(u,v))+\frac{siz_v}{siz_u+siz_v}\cdot(\sum_{x\in son_v} f(u,x)+g(v,u)) \]

其中 \(g(u,v)\) 表示子树 \(v\) 中比 \(u\) 小的数的个数,\(siz_u\) 表示子树 \(u\) 的大小,前面的系数就是考虑有 \(\frac{siz_u}{siz_u+siz_v}\) 的概率 \(u\) 放在第一位,因为这相当于从 \(siz_u+siz_v\) 个数中选出 \(siz_u\) 个,选到红色的概率。然后计算 \(u\) 和 子树 \(v\) 的贡献,就可以递归到子问题了,后面那部分的解释类似。

总状态数 \(O(n^2)\),转移均摊 \(O(1)\),所以时间复杂度 \(O(n^2)\)

总结

多想一下我再总结吧。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 4100;
const int MOD = 1e9+7;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,rt,tot,ans,f[M],fa[M];
int dp[M][M],inv[M],siz[M],g[M][M];
struct edge{int v,next;}e[M<<1];
void dfs(int u,int p)
{
	siz[u]=1;fa[u]=p;
	for(int i=u+1;i<=n;i++) g[i][u]++;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(v==p) continue;
		dfs(v,u);siz[u]+=siz[v];
		for(int j=1;j<=n;j++) g[j][u]+=g[j][v];
	}
}
int get(int x,int y)
{
	if(~dp[x][y]) return dp[x][y];
	int res=(siz[x]*g[x][y]+siz[y]*g[y][x])%MOD;
	for(int i=f[x];i;i=e[i].next) if(e[i].v^fa[x])
		res=(res+siz[x]*get(e[i].v,y))%MOD;
	for(int i=f[y];i;i=e[i].next) if(e[i].v^fa[y])
		res=(res+siz[y]*get(x,e[i].v))%MOD;
	res=res*inv[siz[x]+siz[y]]%MOD;
	return dp[y][x]=dp[x][y]=res;
}
signed main()
{
	freopen("nightmare.in","r",stdin);
	freopen("nightmare.out","w",stdout); 
	n=read();rt=read();inv[0]=inv[1]=1;
	for(int i=2;i<=n;i++)
		inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		e[++tot]=edge{u,f[v]},f[v]=tot;
		e[++tot]=edge{v,f[u]},f[u]=tot;
	}
	memset(dp,-1,sizeof dp);
	dfs(rt,0);
	for(int i=1;i<=n;i++) ans+=g[i][i];
	for(int i=1;i<=n;i++)
	{
		for(int j=f[i];j;j=e[j].next)
		{
			int u=e[j].v;
			if(u==fa[i]) continue;
			for(int k=f[i];k;k=e[k].next)
			{
				int v=e[k].v;
				if(v==fa[i]) continue;
				if(u<v) ans=(ans+get(u,v))%MOD; 
			}
		}
	}
	printf("%lld\n",ans);
}
上一篇:升级中的猫 (Cats on the Upgrade, CF1625E)


下一篇:Ubuntu下配置workon切换虚拟环境