题面

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

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

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. JS---part2课程介绍+part1复习

    part1复习 JavaScript分三个部分: 1. ECMAScript标准----JS基本的语法 2. DOM:Document Object Model 文档对象模型 3. BOM:浏览器对象 ...

  2. AWS云EC2(RHEL7)添加网络接口与路由调整

    AWS云EC2(RHEL7)添加网络接口与路由调整 Amazon Linux(类似RHEL6,Centos6) 以及 RHEL7 修改MAC地址的说明 RHEL7 Centos7 添加路由 解决RHE ...

  3. IIS配置和发布网站

    一.安装配置IIS 控制面板->程序和功能->启用或关闭Windows功能 选中“Internet Information Services”,勾选Web管理工具子项,万维网服务子项(万维 ...

  4. Step by Step Process of Migrating non-CDBs and PDBs Using ASM for File Storage (Doc ID 1576755.1)

    Step by Step Process of Migrating non-CDBs and PDBs Using ASM for File Storage (Doc ID 1576755.1) AP ...

  5. 连接 sql

    java连接sqlserver 1 创建 Dynamic Web Project项目 在WebContent/WEB-INF/lib中添加sqljdbc42.jar 2 在class文件里连接数据库 ...

  6. 逆向学习周记-C语言空函数

    实验环境:WIN7虚拟机 软件:VC6 首先在VC6里面写一个空函数Fun(): F7编译运行一下,没有出错,接着在函数处使用F9下断点,使程序运行到Fun函数时停下. 接着F5开始运行这个程序 程序 ...

  7. nginx基础(3)

    目录 HTTP首部 1.通用首部 2.请求首部 2.1 必有首部 2.2 条件请求首部 2.3 安全相关首部 3.响应首部 3.1 必有首部 3.2 协商首部 3.3 安全相关首部 4.实体首部 4. ...

  8. ArrayList 与数组的“纠缠不清”的暧昧

    目录 前言 正话(个人的见解,有误请多指教) 惯例先明白它是什么? 那么它有什么用呢? 怎么用 前言 能不能有一种数组可以在删除掉某些元素自动缩小就好了.可是话说哪里学的Java?数组能删除元素吗?今 ...

  9. PalletOne调色板Token PTN跨链转网的技术原理

    之前一直在忙于通用跨链公链PalletOne的研发,没有怎么做技术分享的博客,最近PalletOne主网上线也有几个月的时间了,即将进行PTN(PalletOne上面的主Token)从ERC20到主网 ...

  10. 利用Python开发智能阅卷系统

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者: 机器学习与统计学 PS:如有需要Python学习资料的小伙伴可以加 ...