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);
}