Atcoder Grand Contest 009&010

009E Eternal Average

题目描述

点此看题

解法

本题的操作是树形结构,所以我们可以直接去考虑最后的结果而不去考虑过程。

可以把操作看成一棵 \(k\) 叉树,叶子代表初始的数。设权值为 \(1\) 的点深度是 \(x_i\),权值为 \(0\) 的点的深度是 \(y_i\),那么根节点的数一定是 \(\sum k^{-x_i}\),此外这棵树能构造出的充要条件是:\(\sum k^{-x_i}+\sum k^{-y_i}=1\)

原题自动机 \(\tt C202044zxy\) 提醒您,这里有一道结论一模一样的题目:To make 1

那么我们想在合法的基础上求出以 \(\sum k^{-x_i}\) 区分的方案数,这里我们尽量往数位 \(dp\) 的方向靠。主要问题是并不满足 \(0\leq x_i<k\) 的关键条件,我们尝试转化问题把这个关键条件给搞出来。

根据 \(1\) 的个数固定我们可以写出:\(\sum x_i=n\),那么我们考虑进位之后的 \(x_i'\),只要满足 \(\sum x_i'\leq n\and \sum x_i'=n\bmod (k-1)\) 就一定对应着一个原来的 \(x\) 序列,这样我们可以很方便的用 \(x\) 去做数位 \(dp\)

根据 \(0\) 的个数固定和总和为 \(1\) 我们可以写出:\(1+\sum_{i=1}^{len}(k-1-x_i)=m\),设 \(t=(k-1)\cdot len-\sum x_i+1\),那么做同样的转化可以得到 \(t\leq m\and t=m\bmod (k-1)\)

\(f[i][j][0/1]\) 表示考虑到第 \(i\) 位,\(\sum x_i=j\)\(x_i=0/1\) 的方案数,由于我们要保证 \(len\) 才能判断上述条件,所以我们在保证 \(x_i=1\) 的情况下统计满足上述条件的方案数即可,时间复杂度 \(O(\frac{n^2}{k})\)

总结

这种操作之后除以某个数的题目通常有树形结构,通过树形结构可以导出一些结果向的结论。

本题的转化方便了数位 \(dp\),这说明可以考虑进位之后的数列有什么性质,在原序列和新序列之间建立等价关系

#include <cstdio>
const int M = 2005;
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,m,k,ans,f[M<<1][M][2],g[M];
signed main()
{
	n=read();m=read();k=read();
	f[0][0][0]=1;
	for(int i=1,up=(n+m-1)/(k-1);i<=up;i++)
	{
		g[0]=(f[i-1][0][0]+f[i-1][0][1])%MOD;
		for(int j=1;j<=n;j++)
			g[j]=(g[j-1]+f[i-1][j][0]+f[i-1][j][1])%MOD;
		f[i][0][0]=g[0];
		for(int j=1;j<=n;j++)
		{
			f[i][j][0]=(g[j]-g[j-1])%MOD;
			f[i][j][1]=(g[j-1]-(j-k>=0?g[j-k]:0)+MOD)%MOD;
		}
		for(int j=1;j<=n;j++)
		{
			int t=1+(k-1)*i-j;
			if(j%(k-1)==n%(k-1) && t<=m && t%(k-1)==m%(k-1))
				ans=(ans+f[i][j][1])%MOD;
		}
	}
	printf("%lld\n",ans);
}

010E Rearranging

题目描述

点此看题

解法

这种确定顺序的题,常用的方法是思考不变的相对顺序

本题我们观察到不互质的数一定不会被后手改变相对顺序,并且考虑到相对顺序通常是用图论来表示的,所以我们把不互质的数之间连一条边,这样会得到一个无向图。

因为互质数的相对顺序是可以任意改变的,先手决定的只有互质的数的相对顺序。考虑先手的功能是给无向图定向(若 \(i\rightarrow j\) 则表示 \(p_i<p_j\),定向完一定是个拓扑图),后手的功能是给这个无向图定下字典序最大的拓扑序。

先手的定向具有固定策略,首先值最小的点一定作为第一个点,然后我们考虑和它相连边的方向都固定了。然后考虑把下一个位置优先给值更小的点 \(x\) ,发现此时我们可以把 \(x\) 当根,这是一个递归的过程:

void dfs(int u)
{
	vis[u]=1;
	for(int v=1;v<=n;v++)
		if(!vis[v] && b[u][v])
			g[u].pb(v),dfs(v);
}

我们只需要保留在 \(\tt dfs\) 树上的边(因为非树边对顺序没有更强的限制),这种方法为什么是正确的呢?我的理解是我们让值小点的位置尽量小,就使他处在偏序关系的上层,但如果子树间无关联那么顺序是任意的。

如果有更加理性的证明请联系我,上面的文字就是我的一些感性理解

#include <cstdio>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
const int M = 2005;
#define pb push_back
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,a[M],b[M][M],vis[M];
priority_queue<int> q;vector<int> g[M],ans;
int gcd(int a,int b) {return !b?a:gcd(b,a%b);}
void dfs(int u)
{
	vis[u]=1;
	for(int v=1;v<=n;v++)
		if(!vis[v] && b[u][v])
			g[u].pb(v),dfs(v);
}
void topo()
{
	while(!q.empty())
	{
		int u=q.top();q.pop();ans.pb(u);
		for(int v:g[u]) q.push(v);
	}
}
signed main()
{
	freopen("rearranging.in","r",stdin);
	freopen("rearranging.out","w",stdout);
	n=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<=n;j++)
			b[i][j]=b[j][i]=(gcd(a[i],a[j])>1);
	for(int i=1;i<=n;i++) if(!vis[i])
		q.push(i),dfs(i);
	topo();
	for(int x:ans) printf("%d ",a[x]);puts("");
}

009D Uninity

题目描述

点此看题

解法

首先不难把题目转化为重构一棵点分树(但是并不是传统意义上的按重心划分),使得最大深度最小。因为这东西我都想得出来肯定很简单所以就不证明了,同时我们可以发现一个性质就是答案 \(\leq \log n\)(按传统点分树划分就可以得到上界)

考虑进一步转化,因为我们想在固定结构上规划。考虑点分树是可以和路径表示法互化的,那么我们可以把题意转化成给每个点定一个标号 \(\tt label\)使得最后任意两个 \(\tt label\) 相等的点树上路径(原树)存在点的 \(\tt label\) 大于它。证明这个转化是很简单的,也就是考虑这两点同层,那么路径一定会过点分树的某个根,而根的 \(\tt label\) 大于它就是合法点分树。

那么可以在原树上规划 \(\tt label\),设 \(g[u]\) 表示子树 \(u\) 中未解决的 \(\tt label\) 集合状压起来的值(因为答案 \(\leq \log n\) 所以存得下 ),注意这里如果子树内某个点 \(v\)\(u\) 的路径上存在 \(label[x]>label[v]\) 就视为 \(v\) 已解决。

那么我们考虑合并子树的过程中会新增多少限制,发现就是位运算取并,我们再把取并的结果或起来就得到了总的限制。那么首先这个点的 \(\tt label\) 必须要足够大,此外不能和子树中未解决点的 \(\tt label\) 相同(要不然就直接不合法了)

实现就暴力增大 \(\tt label\),时间复杂度 \(O(n\log n)\),本题最后的做法本质上是一个树上延迟贪心模型

总结

规划问题中,把动态结构转化成固定结构可以用标号法,要求 \(\tt label\) 可以充分描述动态结构的性质。

//The second I waste is more than I can take. 
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 100005;
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,tot,ans,f[M],g[M];
struct edge{int v,next;}e[M<<1];
void dfs(int u,int fa)
{
	int s=0,dp=0;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(v==fa) continue;
		dfs(v,u);s|=g[u]&g[v];g[u]|=g[v];
	}
	while(s) dp++,s>>=1;
	while(g[u]&(1<<dp)) dp++;
	g[u]=((g[u]>>dp)|1)<<dp;
	ans=max(ans,dp);
}
signed main()
{
	n=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;
	}
	dfs(1,0);
	printf("%d\n",ans);
}
posted @ 2022-02-12 17:13  C202044zxy  阅读(340)  评论(0编辑  收藏  举报