CSP2019 树上的数 题解
题面
这是一道典型的部分分启发正解的题。
所以我们先来看两个部分分。
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号点,那么需要满足的条件有两种:
- (3)的优先级是与1号点相邻的边中最大的,(6)的优先级是与3号点相邻的边中最小的;
- (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 树上的数 题解的更多相关文章
- [CSP-S2019]树上的数 题解
CSP-S2 2019 D1T3 考场上写了2h还是爆零……思维题还是写不来啊 思路分析 最开始可以想到最简单的贪心,从小到大枚举每个数字将其移动到最小的节点.但是通过分析样例后可以发现,一个数字在移 ...
- 【CSP2019】树上的数
[CSP2019]树上的数 题面 洛谷 题解 我们设每个点上的编号分别为\(a_1,a_2...a_n\). 10pts ... 菊花 假设现在菊花中心编号是\(rt\),设你依次拆边\((p_1,r ...
- C#版 - Leetcode 504. 七进制数 - 题解
C#版 - Leetcode 504. 七进制数 - 题解 Leetcode 504. Base 7 在线提交: https://leetcode.com/problems/base-7/ 题目描述 ...
- C#版 - Leetcode 306. 累加数 - 题解
版权声明: 本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C#版 - L ...
- C#版(打败97.89%的提交) - Leetcode 202. 快乐数 - 题解
版权声明: 本文为博主Bravo Yeung(知乎UserName同名)的原创文章,欲转载请先私信获博主允许,转载时请附上网址 http://blog.csdn.net/lzuacm. C#版 - L ...
- CSP2019 D1T3 树上的数 (贪心+并查集)
题解 因为博主退役了,所以题解咕掉了.先放个代码 CODE #include<bits/stdc++.h> using namespace std; const int MAXN = 20 ...
- BZOJ3930:[CQOI2015]选数——题解
http://www.lydsy.com/JudgeOnline/problem.php?id=3930 https://www.luogu.org/problemnew/show/P3172#sub ...
- 洛谷P1037 产生数 题解 搜索
题目链接:https://www.luogu.com.cn/problem/P1037 题目描述 给出一个整数 \(n(n<10^{30})\) 和 \(k\) 个变换规则 \((k \le 1 ...
- 洛谷P1036 选数 题解 简单搜索/简单状态压缩枚举
题目链接:https://www.luogu.com.cn/problem/P1036 题目描述 已知 \(n\) 个整数 \(x_1,x_2,-,x_n\) ,以及 \(1\) 个整数 \(k(k& ...
随机推荐
- 搭建ES集群
服务版本选择 TEG的ctsdb当前最高版本采用的是es的6.4.3版本,为了日后与ctsdb衔接方便,部署开源版es时也采用该版本.6.4.3版本的es依赖的jdk版本要求在8u181以上,测试环境 ...
- 6、netty第五个例子,使用websocket来通讯
websocket中,可以支持双向的数据通信.其中所有的数据格式,都是以帧的形式来传递. initializer import io.netty.channel.ChannelInitializer; ...
- gitlab忘记密码如何重置
gitlab web登入密码忘记以后可以用如下方式修改密码shell>cd /home/git/gitlabshell> su gitshell>bundle exec rails ...
- LeetCode刷题--整数反转(简单)
题目描述 给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转. 示例 1: 输入: 123 输出: 321 示例 2: 输入: -123 输出: -321 示例 3: 输入: 12 ...
- 对于文本生成类4种评价指标的的计算BLEU METEOR ROUGE CIDEr
github下载链接:https://github.com/Maluuba/nlg-eval 将下载的文件放到工程目录,而后使用如下代码计算结果 具体的写作格式如下: from nlgeval imp ...
- 【Spring JDBC】NamedParameterJdbcTemplate(四)
一.什么是具名参数 在经典的 JDBC 用法中, SQL 参数是用占位符 ? 表示,并且受到位置的限制.定位参数的问题在于, 一旦参数的顺序发生变化, 就必须改变参数绑定.在 Spring JDBC ...
- 关于java中三种初始化块的执行顺序
许多小伙伴对于java中的三种初始化块的执行顺序一直感到头疼,接下来我们就来分析一下这三种初始化块到底是怎么运行的.有些公司也会将这个问题作为笔试题目. 下面通过一段代码来看看创建对象时这么初始化块是 ...
- IT兄弟连 HTML5教程 CSS3揭秘 小结及习题
小结 CSS3对于开发者来说,给web应用带来了更多的可能性,极大提高了开发效率.CSS3在选择器上的支持可谓是丰富多彩,使得我们能够灵活的控制样式,而不必为元素进行规范化的命名.CSS3支持的动画类 ...
- 【Unity游戏开发】性能优化之在真机上开启DeepProfile与踩坑
一.引子 最近马三入职了新公司,平时除了负责编辑器开发之外还要做一些游戏性能优化方面的工作.在这里首先给大家安利一下Unity官方的性能测试分析工具URP ,这个工具目前是免费,测试的过程中也不需要接 ...
- Web安全测试学习笔记-DVWA-存储型XSS
XSS(Cross-Site Scripting)大致分为反射型和存储型两种,之前对XSS的认知仅停留在如果网站输入框没有屏蔽类似<script>alert('ok')</scrip ...