题面

这是一道典型的部分分启发正解的题。

所以我们先来看两个部分分。

Part 1 菊花图###

这应该是除了暴力以外最好想的一档部分分了。

如上图(节点上的数字已省略),如果我们依次删去边(2)(1)(3)(4),那么操作完后2号点上的数字就会跑到1号点上,1号点数字会跑到3号点上,3号点数字跑到4号点上……依此累推。那么我们相当于把五个节点连成了一个环( 5 -> 2 -> 1 -> 3 -> 4 -> 5 ),每一个结点上的数字都会跑到环上的下一个结点上去,我们就是要求能使最终得到的排列字典序最小的环。那么我们逐位贪心,先由数字1所在的节点选择它在环上的下一个点是哪一个,在由数字2所在节点选,依此类推。每次在合法的情况下选标号最小的节点即可。具体细节可以见代码。

Part1 代码:

#include<bits/stdc++.h>
using namespace std;
#define N 4007
#define mem(x) memset(x,0,sizeof(x))
int p[N],ans[N],vis[N];
int fa[N];
int find(int x)
{
return fa[x]==x?x:fa[x]=find(fa[x]);
}
int main()
{
int n,t;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
fa[i]=i;
mem(vis);
for(int i=1;i<=n;i++)
scanf("%d",&p[i]);
int x,y;
for(int i=1;i<n;i++)
scanf("%d%d",&x,&y);
for(int i=1;i<=n;i++)
{
int x=p[i];
for(int j=1;j<=n;j++)
{
if(!vis[j]&&(i==n||find(x)!=find(j)))
{
ans[i]=j;
fa[find(x)]=find(j);
vis[j]=1;
break;
}
}
}
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
printf("\n");
}
return 0;
}

Part2 链###

这一部分和正解关系紧密,引入了拓扑序的模型来描述题目中的限制条件。

其中节点中括号里的是节点上的数字。

如果数字1想跑到1号节点上去要怎么办?

那么我们可以依次删去(2)(3)号边,那么数字1就在1号点上了。

只要这样就可以了吗?

其实这里还隐含了两个条件,就是边(1)必须在(2)之后删除,且(4)必须在(3)之前删除,不然数字1就不在它应该在的地方了。

如果我们为每一条边定一个优先级,优先级大的先删,那么4条边的优先级大小关系就是:(1) < (2) > (3) < (4)

这样我们就可以保证数字1最终在1号点上了,此外因为我们要逐位贪心,所以在此基础上我们还希望数字2能到2号节点上。

但这是否有可能呢?

发现这样是不可能的,因为要使数字2到达2号点,那么必须满足优先级(3) > (2) , 与数字1的条件是冲突的,所以不行。

所以我们得到了这部分的算法:

从数字1到数字n逐位贪心,每次选择一个当前数字能到达的、不与之前条件冲突的、标号最小的节点,作为这个数字最终所在的节点。然后将新产生的条件加入。

具体的实现可以在每一个节点上维护一个标记值0,1,2,分别表示这个节点左右的两条边之间的优先级 没有限制、左边大于右边、右边大于左边。然后每次从当前数字所在位置向左右两边找符合条件的点,再把新条件对应的标记值更新即可。具体见代码。

Part2 代码:

#include<bits/stdc++.h>
using namespace std;
#define N 4007
int hd[N],pre[N],to[N],num,tag[N],pos[N],id[N],p[N],d[N],cnt,ans[N];
void adde(int x,int y)
{
num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
void dfs(int v,int f)
{
id[++cnt]=v,pos[v]=cnt;
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(u==f)continue;
dfs(u,v);
}
}
#define mem(x) memset(x,0,sizeof(x))
int main()
{
int n;
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
mem(hd),mem(tag),mem(d);
num=0,cnt=0;
for(int i=1;i<=n;i++)
scanf("%d",&p[i]);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
adde(x,y),adde(y,x);
d[x]++,d[y]++;
}
int rt=0;
for(int i=1;i<=n ;i++)
if(d[i]==1)rt=i;
dfs(rt,0);
for(int i=1;i<=n;i++)
{
int x=p[i],b=pos[x];
int mi=n+1;
if(tag[b]!=1)
{
for(int j=b+1;j<=n;j++)
{
if(tag[j]!=1)mi=min(mi,id[j]);
if(tag[j]==2)break;
}
}
if(tag[b]!=2)
{
for(int j=b-1;j>=1;j--)
{
if(tag[j]!=2)mi=min(mi,id[j]);
if(tag[j]==1)break;
}
}
if(pos[mi]>b)
{
for(int j=b+1;j<=pos[mi]-1;j++)tag[j]=1;
tag[b]=tag[pos[mi]]=2;
}
else
{
for(int j=pos[mi]+1;j<b;j++)tag[j]=2;
tag[pos[mi]]=tag[b]=1;
}
tag[1]=tag[n]=0;
ans[i]=mi;
}
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
printf("\n");
}
return 0;
}

Part3 正解###

其实从第二部分我们已经看到,形如数字x要到y号节点上所需满足的条件可以描述为一系列边的优先级的大小关系(这里的优先级实际上就是一种拓扑序),并且我们可以把这些条件放在节点上,表示与这个节点相邻的所有边之间的大小关系是怎样的,也就是说只有与同一个节点相邻的边之间才会有大小关系的限制。那么我们如何把链上的算法拓展到一般的树上呢?

如果有一个数字想从1号点到3号点,那么需要满足的条件有两种:

  1. (3)的优先级是与1号点相邻的边中最大的,(6)的优先级是与3号点相邻的边中最小的;
  2. (3)的优先级大于(6);

对于条件2,这样还不够充分,因为如果删完(3)之后再删(4)的话就不对了。也就是说删完(3)之后要紧接着删(6)

这个限制条件就相当于把与2号点相邻的边按照优先级大小排列,那么(3)和(6)必须是相邻的,且(3)在(6)前面。

怎么做到这一点呢?发现这种要让两条边相邻的条件其实已经在第一部分的菊花图中讨论过了,一样的用并查集判断即可,只不过Part1中只用了一个并查集,而现在我们要维护每一个点周围的边的大小关系,所以要对每一个点开一个并查集。

而对于条件1,我的做法是对每一个并查集建了一个虚点,如果一条边的优先级最大,那么由虚点向它连一条边,如果一条边优先级最小,则由它向虚点连一条边,这样就可以直接套Part1部分的代码了。

然后贪心过程与Part2类似,每次从一个点出发遍历整棵树,每走一条边就判断一下,然后再对一条路径进行修改即可。

Part3 代码:

#include<bits/stdc++.h>
using namespace std;
#define N 20007
#define mem(x) memset(x,0,sizeof(x))
int hd[N],pre[N],to[N],num,fa[N],sz[N],d[N],p[N],ver;
bool in[N],out[N];
void adde(int x,int y)
{
num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
int find(int x)
{
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
int u=find(x),v=find(y);
fa[u]=v,sz[v]+=sz[u];
out[x]=in[y]=1;
}
bool check(int x,int y,int l)
{
if(in[y]||out[x])return false;
int u=find(x),v=find(y);
if(u==v&&sz[u]!=l)return false;
return true;
}
void dfs1(int v,int f)
{
if(f!=v&&check(f,v,d[v]+1))ver=min(ver,v);
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(i==f)continue;
if(check(f,i,d[v]+1))
{
dfs1(u,i^1);
}
}
}
bool dfs2(int v,int f,int p)
{
if(v==p)
{
merge(f,v);
return true;
}
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(i==f)continue;
if(dfs2(u,i^1,p))
{
merge(f,i);
return true;
}
}
return false;
}
int main()
{
int t,n;
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
mem(hd),mem(in),mem(out),mem(d);
num=(n+1)/2*2+1;
for(int i=1;i<=n;i++)
scanf("%d",&p[i]);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
d[x]++,d[y]++;
adde(x,y),adde(y,x);
}
for(int i=1;i<=num;i++)
fa[i]=i,sz[i]=1;
for(int i=1;i<=n;i++)
{
int v=p[i];
ver=n+1;
dfs1(v,v);
dfs2(v,v,ver);
printf("%d ",ver);
}
printf("\n");
}
return 0;
}

总结:通过这题大家可以发现,此题正解与部分分是紧密相连的,如果没有对部分分的思考,很难直接想到正解。这启发我们当无法直接想到正解时,可以思考一些此题的部分分,找到部分分与正解之间的联系,进而以迂回的方式找到正解。一些人因过于追求正解,直接跳过部分分思考正解,结果反而无法得到正解。比如本文作者就是这样一个反面例子

CSP2019 树上的数 题解的更多相关文章

  1. [CSP-S2019]树上的数 题解

    CSP-S2 2019 D1T3 考场上写了2h还是爆零……思维题还是写不来啊 思路分析 最开始可以想到最简单的贪心,从小到大枚举每个数字将其移动到最小的节点.但是通过分析样例后可以发现,一个数字在移 ...

  2. 【CSP2019】树上的数

    [CSP2019]树上的数 题面 洛谷 题解 我们设每个点上的编号分别为\(a_1,a_2...a_n\). 10pts ... 菊花 假设现在菊花中心编号是\(rt\),设你依次拆边\((p_1,r ...

  3. C#版 - Leetcode 504. 七进制数 - 题解

    C#版 - Leetcode 504. 七进制数 - 题解 Leetcode 504. Base 7 在线提交: https://leetcode.com/problems/base-7/ 题目描述 ...

  4. C#版 - Leetcode 306. 累加数 - 题解

    版权声明: 本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C#版 - L ...

  5. C#版(打败97.89%的提交) - Leetcode 202. 快乐数 - 题解

    版权声明: 本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C#版 - L ...

  6. CSP2019 D1T3 树上的数 (贪心+并查集)

    题解 因为博主退役了,所以题解咕掉了.先放个代码 CODE #include<bits/stdc++.h> using namespace std; const int MAXN = 20 ...

  7. BZOJ3930:[CQOI2015]选数——题解

    http://www.lydsy.com/JudgeOnline/problem.php?id=3930 https://www.luogu.org/problemnew/show/P3172#sub ...

  8. 洛谷P1037 产生数 题解 搜索

    题目链接:https://www.luogu.com.cn/problem/P1037 题目描述 给出一个整数 \(n(n<10^{30})\) 和 \(k\) 个变换规则 \((k \le 1 ...

  9. 洛谷P1036 选数 题解 简单搜索/简单状态压缩枚举

    题目链接:https://www.luogu.com.cn/problem/P1036 题目描述 已知 \(n\) 个整数 \(x_1,x_2,-,x_n\) ,以及 \(1\) 个整数 \(k(k& ...

随机推荐

  1. 华为开发者联盟 方舟编译器 DevEco IDE

    华为开发者联盟 https://developer.huawei.com/consumer/cn/ 方舟编译器是为支持多种编程语言.多种芯片平台的联合编译.运行而设计的统一编程平台,包含编译器.工具链 ...

  2. java核心API

    ---恢复内容开始--- Javase01 day01 关于String: String是不可变对象,java.lang.String使用了final修饰,不能被继承: 字符串一旦创建永远无法改变,但 ...

  3. Git介绍与简易搭建

    Git介绍 Git(读音为/gɪt/.)是一个开源的分布式版本控制系统,可以有效.高速的处理从很小到非常大的项目版本管理. Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发 ...

  4. java中文件复制的4种方式

    今天一个同事问我文件复制的问题,他一个100M的文件复制的指定目录下竟然成了1G多,吓我一跳,后来看了他的代码发现是自己通过字节流复制的,定义的字节数组很大,导致复制后目标文件非常大,其实就是空行等一 ...

  5. go 杂项笔记

    *** 使用go build编译该程序,注意这里需要指定 -gcflags "-N -l" 关闭编译器优化,否则编译器可能把对sum函数的调用优化掉. bobo@ubuntu:~/ ...

  6. 201871010113-刘兴瑞《面向对象程序设计(java)》第十七周学习总结

    项目 内容 这个作业属于哪个课程 <任课教师博客主页链接>https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 <作业链接地址>http ...

  7. 字典树(Trie)详解

    详解字典树(Trie) 本篇随笔简单讲解一下信息学奥林匹克竞赛中的较为常用的数据结构--字典树.字典树也叫Trie树.前缀树.顾名思义,它是一种针对字符串进行维护的数据结构.并且,它的用途超级广泛.建 ...

  8. 3种不走寻常路的黑客攻击泄露&如何保护自己?

    数据泄露和黑客攻击现在已经成为我们日常生活中的常见部分,除非您是网络安全专业人员或您的个人数据受到威胁,否则您实际上并不关心是否存在新的漏洞. 正如美国联邦贸易委员会指出的那样,万豪酒店连锁店的超过5 ...

  9. 浅谈Kotlin中的函数

    本文首发于 vivo互联网技术 微信公众号 链接:https://mp.weixin.qq.com/s/UV23Uw_969oVhiOdo4ZKAw作者:连凌能 Kotlin,已经被Android官方 ...

  10. SVM算法核函数的选择

    SVM支持向量机,一般用于二分类模型,支持线性可分和非线性划分.SVM中用到的核函数有线性核'linear'.多项式核函数pkf以及高斯核函数rbf. 当训练数据线性可分时,一般用线性核函数,直接实现 ...