题面

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

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

Part 1 菊花图###

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

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

Part1 代码:

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define N 4007
  4. #define mem(x) memset(x,0,sizeof(x))
  5. int p[N],ans[N],vis[N];
  6. int fa[N];
  7. int find(int x)
  8. {
  9. return fa[x]==x?x:fa[x]=find(fa[x]);
  10. }
  11. int main()
  12. {
  13. int n,t;
  14. scanf("%d",&t);
  15. while(t--)
  16. {
  17. scanf("%d",&n);
  18. for(int i=1;i<=n;i++)
  19. fa[i]=i;
  20. mem(vis);
  21. for(int i=1;i<=n;i++)
  22. scanf("%d",&p[i]);
  23. int x,y;
  24. for(int i=1;i<n;i++)
  25. scanf("%d%d",&x,&y);
  26. for(int i=1;i<=n;i++)
  27. {
  28. int x=p[i];
  29. for(int j=1;j<=n;j++)
  30. {
  31. if(!vis[j]&&(i==n||find(x)!=find(j)))
  32. {
  33. ans[i]=j;
  34. fa[find(x)]=find(j);
  35. vis[j]=1;
  36. break;
  37. }
  38. }
  39. }
  40. for(int i=1;i<=n;i++)
  41. printf("%d ",ans[i]);
  42. printf("\n");
  43. }
  44. return 0;
  45. }

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 代码:

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define N 4007
  4. int hd[N],pre[N],to[N],num,tag[N],pos[N],id[N],p[N],d[N],cnt,ans[N];
  5. void adde(int x,int y)
  6. {
  7. num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
  8. }
  9. void dfs(int v,int f)
  10. {
  11. id[++cnt]=v,pos[v]=cnt;
  12. for(int i=hd[v];i;i=pre[i])
  13. {
  14. int u=to[i];
  15. if(u==f)continue;
  16. dfs(u,v);
  17. }
  18. }
  19. #define mem(x) memset(x,0,sizeof(x))
  20. int main()
  21. {
  22. int n;
  23. int t;
  24. scanf("%d",&t);
  25. while(t--)
  26. {
  27. scanf("%d",&n);
  28. mem(hd),mem(tag),mem(d);
  29. num=0,cnt=0;
  30. for(int i=1;i<=n;i++)
  31. scanf("%d",&p[i]);
  32. for(int i=1;i<n;i++)
  33. {
  34. int x,y;
  35. scanf("%d%d",&x,&y);
  36. adde(x,y),adde(y,x);
  37. d[x]++,d[y]++;
  38. }
  39. int rt=0;
  40. for(int i=1;i<=n ;i++)
  41. if(d[i]==1)rt=i;
  42. dfs(rt,0);
  43. for(int i=1;i<=n;i++)
  44. {
  45. int x=p[i],b=pos[x];
  46. int mi=n+1;
  47. if(tag[b]!=1)
  48. {
  49. for(int j=b+1;j<=n;j++)
  50. {
  51. if(tag[j]!=1)mi=min(mi,id[j]);
  52. if(tag[j]==2)break;
  53. }
  54. }
  55. if(tag[b]!=2)
  56. {
  57. for(int j=b-1;j>=1;j--)
  58. {
  59. if(tag[j]!=2)mi=min(mi,id[j]);
  60. if(tag[j]==1)break;
  61. }
  62. }
  63. if(pos[mi]>b)
  64. {
  65. for(int j=b+1;j<=pos[mi]-1;j++)tag[j]=1;
  66. tag[b]=tag[pos[mi]]=2;
  67. }
  68. else
  69. {
  70. for(int j=pos[mi]+1;j<b;j++)tag[j]=2;
  71. tag[pos[mi]]=tag[b]=1;
  72. }
  73. tag[1]=tag[n]=0;
  74. ans[i]=mi;
  75. }
  76. for(int i=1;i<=n;i++)
  77. printf("%d ",ans[i]);
  78. printf("\n");
  79. }
  80. return 0;
  81. }

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 代码:

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define N 20007
  4. #define mem(x) memset(x,0,sizeof(x))
  5. int hd[N],pre[N],to[N],num,fa[N],sz[N],d[N],p[N],ver;
  6. bool in[N],out[N];
  7. void adde(int x,int y)
  8. {
  9. num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
  10. }
  11. int find(int x)
  12. {
  13. return fa[x]==x?x:fa[x]=find(fa[x]);
  14. }
  15. void merge(int x,int y)
  16. {
  17. int u=find(x),v=find(y);
  18. fa[u]=v,sz[v]+=sz[u];
  19. out[x]=in[y]=1;
  20. }
  21. bool check(int x,int y,int l)
  22. {
  23. if(in[y]||out[x])return false;
  24. int u=find(x),v=find(y);
  25. if(u==v&&sz[u]!=l)return false;
  26. return true;
  27. }
  28. void dfs1(int v,int f)
  29. {
  30. if(f!=v&&check(f,v,d[v]+1))ver=min(ver,v);
  31. for(int i=hd[v];i;i=pre[i])
  32. {
  33. int u=to[i];
  34. if(i==f)continue;
  35. if(check(f,i,d[v]+1))
  36. {
  37. dfs1(u,i^1);
  38. }
  39. }
  40. }
  41. bool dfs2(int v,int f,int p)
  42. {
  43. if(v==p)
  44. {
  45. merge(f,v);
  46. return true;
  47. }
  48. for(int i=hd[v];i;i=pre[i])
  49. {
  50. int u=to[i];
  51. if(i==f)continue;
  52. if(dfs2(u,i^1,p))
  53. {
  54. merge(f,i);
  55. return true;
  56. }
  57. }
  58. return false;
  59. }
  60. int main()
  61. {
  62. int t,n;
  63. scanf("%d",&t);
  64. while(t--)
  65. {
  66. scanf("%d",&n);
  67. mem(hd),mem(in),mem(out),mem(d);
  68. num=(n+1)/2*2+1;
  69. for(int i=1;i<=n;i++)
  70. scanf("%d",&p[i]);
  71. for(int i=1;i<n;i++)
  72. {
  73. int x,y;
  74. scanf("%d%d",&x,&y);
  75. d[x]++,d[y]++;
  76. adde(x,y),adde(y,x);
  77. }
  78. for(int i=1;i<=num;i++)
  79. fa[i]=i,sz[i]=1;
  80. for(int i=1;i<=n;i++)
  81. {
  82. int v=p[i];
  83. ver=n+1;
  84. dfs1(v,v);
  85. dfs2(v,v,ver);
  86. printf("%d ",ver);
  87. }
  88. printf("\n");
  89. }
  90. return 0;
  91. }

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

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. Dynamics 365利用Web API对视图进行查询

    我是微软Dynamics 365 & Power Platform方面的工程师罗勇,也是2015年7月到2018年6月连续三年Dynamics CRM/Business Solutions方面 ...

  2. 《Python3 网络爬虫开发实战》开发环境配置过程中踩过的坑

    <Python3 网络爬虫开发实战>学习资料:https://www.cnblogs.com/waiwai14/p/11698175.html 如何从墙内下载Android Studio: ...

  3. Eclipse中如何添加相对路径的外部jar包

    在eclipse中进行java编程的时候,常常需要引用外部jar包.而采用相对路径引用jar包可以大大方便java工程的拷贝,这样使得java工程从一个路径转移到另一个路径时不用大费周章的修改外包ja ...

  4. 25.推荐---协同过滤(Collaborative Filtering)

    协同过滤需要注意的三点: gray sheep(有人喜欢追求特别,协同过滤一般只能从共同的人或物间找相似) shilling attack(水军刷好评导致数据错误,无法带来精确的推荐) cold st ...

  5. Redis学习(一)简介

    REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统. Redis是一个开源的使用ANSI C语言编写.遵守B ...

  6. 【西北师大-2108Java】第十四次作业成绩汇总

    [西北师大-2108Java]第十四次作业成绩汇总 作业题目 面向对象程序设计(JAVA) 第16周学习指导及要求 实验目的与要求 (1)掌握Java应用程序的打包操作: (2)掌握线程概念: (3) ...

  7. java之Objects类

    Objects类概述 在JDK7添加了一个Objects工具类,它提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空 ...

  8. 云数据库MongoDB版清理oplog日志和compact命令详解

    1.问题描述: 今天看到公司mongodb的oplog有点大,看到云数据库MongoDB版日志清理策略. MongoDB数据库在长期频繁地删除/写入数据或批量删除了大量数据,将产生很多物理空间碎片. ...

  9. IT兄弟连 HTML5教程 HTML5的基本语法 小结及习题

    小结 一个完整的HTML文件由标题.段落.列表.表格.文本,即嵌入的各种对象所组成,这些逻辑上统一的对象称为元素.HTML文档主体结构分为两部分,一部分是定义文档类型,另一部分则是定义文档主体的结构框 ...

  10. C++入门到理解阶段二基础篇(9)——C++结构体

    1.概述 前面我们已经了解到c++内置了常用的数据类型,比如int.long.double等,但是如果我们要定义一个学生这样的数据类型,c++是没有的,此时就要用到结构体,换言之通过结构体可以帮我们定 ...