zoukankan      html  css  js  c++  java
  • P5659 [CSP-S2019] 树上的数 题解

    P5659 [CSP-S2019] 树上的数 题解

    前言

    好不容易今天晚上有时间,自己再造一遍,加深一下印象,再总结一下这类问题的思路

    思路

    先来分析一下题目类型,既有节点编号,节点编号上有权值编号,可以实现权值更换,但规定次数,最终按照权值大小输出编号 (其实就是题目大意) 这种操作类就很烦,我看到不是数据结构就是DP,可它出在树上,这就没有任何办法,只能靠自己寻找题目中的性质。这也非常考察一个OI的做题能力,在没有任何算法的情况下去面对联赛的T3,可还行?

    题目最终的要求是让数字从小到大排列得到的序列,因此我们整个题目应该尽可能多注意如何处理数字,而不是关注序号怎么排最小。

    10pts

    数据很小,不会就全部枚举,把所有的交换次数都模拟一遍,最后挨个比较,这样够暴力了,然而这里有考察了全排列正常人很快就会写出来,只有我这愚人半天写不出来

    这里我们用 pair 记录路径,暴力的去交换路径,得到全排列,处理时这里还学到了权值排序的一种算是思想吧,整个题都会贯穿

    for (int i=1;i<=n;i++) mir[a[i]]=i;
    

    我们将每一个权值都用 (mir) 存储数字实际对应的编号,我们的目的是让数字从小到大排序,所以每得到一个序列,完成 (mir) 的赋值,只需要和当前的答案按数字从小到大比较就好了,其实是个人都会

    /*
    	单纯练码,
    	10pts 大约15min
    */
    #include <cmath>
    #include <queue>
    #include <cstdio>
    #include <vector>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    
    const int A = 1e7+10;
    const int B = 1e6+10;
    const int mod = 1e9 + 7;
    const int inf = 0x3f3f3f3f;
    
    inline int read() {
      char c = getchar();
      int x = 0, f = 1;
      for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
      for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
      return x * f;
    }
    pair<int,int>pp[B];
    int a[B],T,n;
    int mir[B],ans[B],vis[B];
    void dfs(int dep)
    {
    	if (dep==n)
    	{
    		for (int i=1;i<=n;i++) mir[a[i]]=i;//里面存的是数字 
    		for (int i=1;i<=n;i++)
    		{
    			if (mir[i]<ans[i])
    			{
    				for (int j=1;j<=n;j++) 
    				{
    					ans[j]=mir[j];
    //					printf("%d ",ans[j]);
    				}
    //				puts("");
    				break;
    			}
    			else if (mir[i]>ans[i]) break;
    		} 
    		return;
    	}
    	for (int i=1;i<n;i++)
    	{
    		if (!vis[i])
    		{
    			int x=pp[i].first,y=pp[i].second;
    			swap(a[x],a[y]); vis[i]=1;
    			dfs(dep+1);
    			swap(a[x],a[y]); vis[i]=0;
    		}
    	}
    }
    void work1()
    {
    	for (int i=1;i<=n;i++) printf("%d ",ans[i]);
    	puts("");
    }
    int main()
    {
    	T=read();
    	while (T--)
    	{
    		n=read();
    		memset(vis,0,sizeof(vis));
    		for (int i=1;i<=n;i++) a[read()]=i,ans[i]=n-i+1;
    		for (int i=1;i<n;i++) {int u=read(),v=read(); pp[i]=make_pair(u,v);}
    		if (n<=10) {dfs(1);work1();}
    		
    	}
    } 
    

    菊花图

    这里先说明一个贪心,这是使全排列(O(n!)) 变成 (O(n^2)) 的方法

    我们期望的,必然是想要小的数字尽量在小的标号前面,看这个例子

    数字 (1) 现在可以和 (1)(5) 换,你选谁,肯定是 (1) 为什么这样选,仅仅是当前状态下更优吗?

    我们在考虑后效性,数字 (1) 位置上如果是 (5) 即使后是最完美的 (1,2, 3, 4,) 的字典序,都不会比数字 (1) 位置上是(1) 的最劣(5,4,3,2) 优,、

    因此能和小数换,决不和大数换,换句话讲,小的数字尽量跟小的编号匹配,剩下的无论好不好,都强制匹配上(只会强制一个,因为小数字前面的匹配到的决不是所拥有状态中最劣的)

    有了这贪心,我们再来看部分分,

    zxsoul 的 sb图

    我们的目的是关注数字而不是编号,因为编号我们可以贪心求得,但前提是如何操作数字,我们随便模拟题目交换顺序,

    (1 o 4)(Y_7 o Y_1) 这里指的是,(Y_7) 来到了 (Y_1) 的位置上,至于 (Y_1) 则来到的花心处

    (1 o 5)(Y_1 o Y_4)(Y_4) 来到花心,

    我们来观察 (Y1) 从开始到完成第二次操作他做了什么?

    (4 o 5) ,直观的讲 (Y_1 o) 原来 (Y_4) 的位置,即位置 (5) ,并且我们发现 (Y1) 不会在移动了,我们不妨记作 (Y_1 o Y_4)

    那么其他非花心的点是否也是这样的操作呢?

    不难发现,是的,并且我们会得到许多类似 (Y_{...} o Y_{...}) 的样子,自己找个小图手摸一下,这个太大了,我不好写出了,总之(比较敷衍,最终构成结束点会在花心结束,并且将所有的 ( o) 连接起来,他构成了一个环

    我们从花心处将环断开就得到了一个链,我这样做有什么用呢?

    但凡是个合理的交换顺序,他从花心处断开一定一条链,绝不会产生环(显而易见

    那么答案必定也是一条链,

    所以找最小链就好了,怎么做还是全排列枚举吗,不用我们上面的贪心,这样就轻而易举的完成了

    所以我们来盘点一下需要处理的东西

    1. 不能有环,并查集查询搞一搞

    2. (O(n^2)) 贪心搞一搞

    3. 如何答案记录?

      ans[mir[i]]=j;//编号变成了j 
      

      代码里呈现了这样的语句,自认为不太好理解,所以来说一下

      解释为:数字大小为 (i) 对应的编号在操作中被更为了 (j)

      for (int i=1;i<=n-1;i++) printf("%d ",ans[mir[i]]);
      	for (int i=1;i<=n;i++) if (!vis[i])	printf("%d",i);
      

      答案查询时,只要从小到大数字就好了,注意,我们保证了不能有环,那么必然有一个数字是没有转移的,因为我们贪心,必然是数字大小为最大一个没有匹配,这时强制匹配就好了,前面都是最优的,并且又是最后一个,形成的序列不就是最优的吗?真优美!

    /*
    	单纯练码,
    	10pts 大约15min
    	35pts 9:00 大约18min 居然这么快, 
    */
    pair<int,int>pp[B];
    int a[B],T,n;
    int mir[B],ans[B],vis[B];
    int du[B],fa[B];
    int find(int x){return (fa[x]==x) ? x : fa[x]=find(fa[x]);} 
    void work2()
    {
    	for (int i=1;i<=n;i++) fa[i]=i;//重置 
    	for (int i=1;i<=n;i++) mir[a[i]]=i;
    	for (int i=1;i<=n;i++)
    		for (int j=1;j<=n;j++)
    		{
    			int fx=find(mir[i]), fy=find(j);
    			if (vis[j] || fx==fy) continue;
    			ans[mir[i]]=j;//编号变成了j 
    			fa[fy]=fx;
    			vis[j]=1;
    			break; 
    		}
    	int flag=0;
    	for (int i=1;i<=n-1;i++) printf("%d ",ans[mir[i]]);
    	for (int i=1;i<=n;i++) if (!vis[i])	printf("%d",i);
    	puts(""); 
    }
    int main()
    {
    //	freopen("tree.in","r",stdin);
    //	freopen("tree.out","w",stdout);
    	T=read();
    	while (T--)
    	{
    		int maxx=0;
    		n=read();
    		memset(vis,0,sizeof(vis));
    		memset(du,0,sizeof(du));
    		for (int i=1;i<=n;i++) a[read()]=i,ans[i]=n-i+1;
    		for (int i=1;i<n;i++) 
    		{
    			int u=read(),v=read(); 
    			du[u]++,du[v]++;
    			pp[i]=make_pair(u,v);
    			maxx=max(du[u],max(du[v],maxx));
    		}
    		if (n<=10) {dfs(1);work1();}
    		else if (maxx==n-1)//菊花图 
    		{
    			work2();
    		}
    		
    	}
    } 
    

    链壮

    首先要想处理链上的东西,就必须知道他们之间的边关系,所以需要建边,得到每个点之间的位置关系

    即:

    void get_path(int u,int pre)
    {
    	tot[pt[u]=++num]=u;
    	for (int i=head[u];i;i=e[i].nxt)
    	{
    		int v=e[i].v;
    		if (v==pre) continue;
    		get_path(v,u);
    	}
    }
    

    再考虑怎么贪心

    贪心的思路没变,就是让小的尽量找小的,那么看下面的情景

    zxsoul 的 sb 图

    假设此时的 (x) 就是最小数字 (1)(i) 就是我们贪心贪的 (1) ,因此我们应该尽可能的让 (x) 来到 (i) 点,我们观察每个点连接的两条边删除的先后顺序,发现 (x o i) 只有 (x,i) 的是先删右边再删左边,路径上的其他点都是先删左边再删右边,这个奇妙的思路我们不妨试一下

    (mark) 标记该点边删除的顺序

    • 未标记-0

    • 先右后左-2

    • 先左后右-1

    • 有了这种限制,我们不妨想让数字(x) 转移到 (j) 点是否可用性,就只需要判断路径上的每一个点的标记是否满足条件,若是右移则为:(211..(1)2),反之:(122..(2)1)

    只要被标记的在以后的贪心里就不会被更改了,换句话说,为了使当前最小,路径上的都强制跟周围两侧的交换了,

    所以我们就做出来了

    写几个我出错的地方

    void ch_l(int x,int y)
    {
    	if (pt[x]!=1 && pt[x]!=n) mark[pt[x]]=1;
    	for (int i=pt[x]+1;i<=pt[y]-1;i++) mark[i]=2;
    	if (pt[y]!=1 && pt[y]!=n) mark[pt[y]]=1;
    }
    

    中间循环的时候是 x+1..y-1 这并不是得到的位置

    if (pt[x]<=pt[y])//需要右移
    			{
    				if (checkl(x,y))//可以右移 也可以认为再次没有被操作过 
    				{
    					ch_r(x,y);
    					flag=1; 
    				} 
    			}
    

    if 没有登号,导致已经被强制修改的点没有更新

    for (int i=pt[x]+1;i<=pt[y]-1;i++) if (mark[i]==2) return 0;
    

    后面的mark[i] 写成 mark[pt[i]] 大意了!

    /*
    	单纯练码,
    	10pts 大约15min
    	35pts 9:00 大约18min 居然这么快, 
    */
    #include <cmath>
    #include <queue>
    #include <cstdio>
    #include <vector>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #define ll long long
    using namespace std;
    
    const int A = 1e7+10;
    const int B = 1e6+10;
    const int mod = 1e9 + 7;
    const int inf = 0x3f3f3f3f;
    
    inline int read() {
      char c = getchar();
      int x = 0, f = 1;
      for ( ; !isdigit(c); c = getchar()) if (c == '-') f = -1;
      for ( ; isdigit(c); c = getchar()) x = x * 10 + (c ^ 48);
      return x * f;
    }
    pair<int,int>pp[B];
    int a[B],T,n;
    int mir[B],ans[B],vis[B];
    void dfs(int dep)
    {
    	if (dep==n)
    	{
    		for (int i=1;i<=n;i++) mir[a[i]]=i;//里面存的是数字 
    		for (int i=1;i<=n;i++)
    		{
    			if (mir[i]<ans[i])
    			{
    				for (int j=1;j<=n;j++) 
    				{
    					ans[j]=mir[j];
    //					printf("%d ",ans[j]);
    				}
    //				puts("");
    				break;
    			}
    			else if (mir[i]>ans[i]) break;
    		} 
    		return;
    	}
    	for (int i=1;i<n;i++)
    	{
    		if (!vis[i])
    		{
    			int x=pp[i].first,y=pp[i].second;
    			swap(a[x],a[y]); vis[i]=1;
    			dfs(dep+1);
    			swap(a[x],a[y]); vis[i]=0;
    		}
    	}
    }
    void work1()
    {
    	for (int i=1;i<=n;i++) printf("%d ",ans[i]);
    	puts("");
    }
    int du[B],fa[B];
    int find(int x){return (fa[x]==x) ? x : fa[x]=find(fa[x]);} 
    void work2()
    {
    	for (int i=1;i<=n;i++) fa[i]=i;//重置 
    	for (int i=1;i<=n;i++) mir[a[i]]=i;
    	for (int i=1;i<=n;i++)
    		for (int j=1;j<=n;j++)
    		{
    			int fx=find(mir[i]), fy=find(j);
    			if (vis[j] || fx==fy) continue;
    			ans[mir[i]]=j;//编号变成了j 
    			fa[fy]=fx;
    			vis[j]=1;
    			break; 
    		}
    	int flag=0;
    	for (int i=1;i<=n-1;i++) printf("%d ",ans[mir[i]]);
    	for (int i=1;i<=n;i++) if (!vis[i])	printf("%d",i);
    	puts(""); 
    }
    struct node{int v,nxt;}e[B];
    int head[B],cnt;
    void modify(int u,int v)
    {
    	e[++cnt].nxt=head[u];
    	e[cnt].v=v;
    	head[u]=cnt;
    }
    int pt[B],num,mark[B],tot[B];// 0 无标记 1 先左后又, 2先右后左 
    void get_path(int u,int pre)
    {
    	tot[pt[u]=++num]=u;
    	for (int i=head[u];i;i=e[i].nxt)
    	{
    		int v=e[i].v;
    		if (v==pre) continue;
    		get_path(v,u);
    	}
    }
    bool checkl(int x,int y)//检查右移满足的条件 
    {
    	if (mark[pt[x]]==1) return 0;
    	for (int i=pt[x]+1;i<=pt[y]-1;i++) if (mark[i]==2) return 0;
    	if (mark[pt[y]]==1) return 0;
    	return true;
    }
    void ch_r(int x,int y)
    {
    	if (pt[x]!=1 && pt[x]!=n) mark[pt[x]]=2;
    	for (int i=pt[x]+1;i<=pt[y]-1;i++) mark[i]=1;
    	if (pt[y]!=1 && pt[y]!=n) mark[pt[y]]=2;
    }
    
    bool checkr(int x,int y)//检查左移满足的条件 
    {
    	if (mark[pt[x]]==2) return 0;
    	for (int i=pt[x]+1;i<=pt[y]-1;i++) if (mark[i]==1) return 0;
    	if (mark[pt[y]]==2) return 0;
    	return true;
    }
    void ch_l(int x,int y)
    {
    	if (pt[x]!=1 && pt[x]!=n) mark[pt[x]]=1;
    	for (int i=pt[x]+1;i<=pt[y]-1;i++) mark[i]=2;
    	if (pt[y]!=1 && pt[y]!=n) mark[pt[y]]=1;
    }
    void work3()
    {
    	num=0;
    	for (int i=1;i<=n;i++) if (du[i]==1) {get_path(i,0); break;}//得到具体位置
    	for (int i=1;i<=n;i++) mir[a[i]]=i,vis[i]=0,mark[i]=0;
    	for (int i=1;i<=n;i++)
    	{	
    		for (int j=1;j<=n;j++) if (!vis[j] && pt[mir[i]]!=pt[j])//二者位置不能重合 
    		{
    			int flag=0;
    			int x=mir[i],y=j; 
    			if (pt[x]<=pt[y])//需要右移
    			{
    				if (checkl(x,y))//可以右移 也可以认为再次没有被操作过 
    				{
    					ch_r(x,y);
    					flag=1; 
    				} 
    			}
    			else 
    			{
    				if (checkr(y,x))//可以左移 
    				{
    					ch_l(y,x);
    					flag=1;
    				}
    			} 
    			if (flag)//可以替换
    			{	
    				ans[i]=j; vis[j]=1;
    				break; 
    			} 
    		}
    	}
    	for (int i=1;i<=n;i++) printf("%d ",ans[i]);
    //	for (int i=1;i<=n;i++) if (!vis[i]) {printf("%d",i);break;}
    	puts("");
    	
    }
    int main()
    {
    //	freopen("tree.in","r",stdin);
    //	freopen("tree.out","w",stdout);
    	T=read();
    	while (T--)
    	{
    		int maxx=0;
    		n=read();
    		cnt=0;
    		memset(head,0,sizeof(head));
    		memset(vis,0,sizeof(vis));
    		memset(du,0,sizeof(du));
    		for (int i=1;i<=n;i++) a[read()]=i,ans[i]=n-i+1;
    		for (int i=1;i<n;i++) 
    		{
    			int u=read(),v=read(); 
    			du[u]++,du[v]++;
    			pp[i]=make_pair(u,v);
    			maxx=max(du[u],max(du[v],maxx));
    			modify(u,v), modify(v,u); 
    		}
    		if (n<=10) {dfs(1);work1();}
    		else if (maxx==n-1)//菊花图 
    		{
    			work2();
    		}
    		else if (maxx==2)
    		{
    			work3();
    		} 
    		
    	}
    } 
    
  • 相关阅读:
    推导式
    解构
    for 循环
    运算符
    while 循环
    流程控制语句
    索引和切片
    ASC转换BCD,ASC2BCD(转)
    CString和char互转,十六进制的BYTE转CString
    C++添加简单的日记记录
  • 原文地址:https://www.cnblogs.com/lToZvTe/p/14851392.html
Copyright © 2011-2022 走看看