题解【luogu5659 树上的数】

CSP-S2 2019 D1T3

考场上写了2h还是爆零……思维题还是写不来啊



思路分析

最开始可以想到最简单的贪心,从小到大枚举每个数字将其移动到最小的节点。但是通过分析样例后可以发现,一个数字在移动的过程中也可能有无关的边的删除,很难处理。显然直接贪心是不可能的。

分析删边对图的影响。可以发现,一条边删去之后,边两端的部分将不会产生任何影响。也就是说,两边的关系只有这一条边。于是还是之前那个贪心的想法,将边的问题转化为点的问题。现在来分析怎么求解。

具体实现

分析样例后可以发现以下性质:

  1. 若某数字要从某个节点的某条边离开,那么这条边一定是这个节点最先被选择的边
  2. 某数字要从某个节点的x边进入,从y边离开,那么x一定先于y被选择,并且这个节点的其它边不在x和y之间被选择
  3. 某数字要最终停在某个节点,那么这个数字通往这个节点的边一定是这个节点最后被选择的边

因此,对于每个节点,关于与其相连的边,有以下三种约束条件:

  1. 某条边最先被选择
  2. 某条边紧跟着另一条边被选择
  3. 某条边最后被选择

我们可以根据上面提到的性质,通过判断每个节点的约束条件是否有冲突来求解方案。

可以发现,对于每个节点,其所有出边构成一条偏序链,我们可以通过维护一个链表来保证约束条件没有冲突。接下来分析一个数字经过一条边会产生什么冲突。假设某数字从$x$到$y$经过了边$(u,v)$。

1.若$x=u$,即$(u,v)$为路径的起始边,$u$为路径的起点
  • $(u,v)$已经被其它数字沿相同方向走过,显然不能再走,不合法
  • $u$上的原数字已走出去,显然不能再走,不合法
  • 已经有数字搬运到$u$,即加上这条边后$u$的边将构成一条完整的偏序链,此时若有其它边不在这条偏序链上,因为要删去所有的边,不合法
2.若$v=y$,即$(u,v)$为路径的终边,$v$为路径的终点
  • $(u,v)$已经被其它数字沿相同方向走过,显然不能再走,不合法
  • $v$已有数字走入,显然不能再走,不合法
  • 已经有数字从$v$搬运出去,即加上这条边后$v$的边将构成一条完整的偏序链,此时若有其它边不在这条偏序链上,因为要删去所有的边,不合法
3.若$x!=u$且$v!=y$,即$(u,v)$为路径的中间部分
  • $(u,v)$已经被其它数字沿相同方向走过,显然不能再走,不合法
  • 加上这条边后$v$的边将构成一条完整的偏序链,此时若有其它边不在这条偏序链上,因为要删去所有的边,不合法
  • 加上这条边后构成的偏序链成为一个环,不合法

偏序链可以用链表O(1)维护。另外,可以发现,每次数字的转移要维护的是一些连续的节点的关系,因此可以用一遍dfsO(n)维护。加上枚举数字的O(n),总的时间复杂度O(n)。

细节比较多,注意不要漏点错点。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=2e3+100;
int T,n,tot;
int head[N],ver[2*N],Next[2*N];
int rev[N],pre[N],cnt1[N],cnt2[N],cnt3[N],from[N],to[N],p[N][N],header[N][N],tailer[N][N];
bool pd[N];
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
    ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
}
void check(int x,int root)//root就是数字的起点
{
    for(int i=head[x],y=ver[i];i;i=Next[i],y=ver[i])
        if(y!=pre[x])
        {
            pre[y]=x,pd[y]=1;
            if(x!=root)
            {
                if(p[x][y]==x || !p[x][y])
                    pd[y]=0;
                if(tailer[x][y]==from[x] && header[x][pre[x]]==to[x] && cnt1[x]*2+cnt2[x]+cnt3[x]-2>0)
                    pd[y]=0;
                if(tailer[x][y]==pre[x])
                    pd[y]=0;
            }//中间边
            else
            {
                if(p[x][y]==x || !p[x][y])
                    pd[y]=0;
                if(from[x] && tailer[x][y]==from[x] && cnt1[x]*2+cnt2[x]+cnt3[x]-1>0)
                    pd[y]=0;
            }//起始边
            pd[y]&=pd[x];//x不行y也不行
            check(y,root);
        }
    if(x==root)
        pd[x]=0;//起终点相同也不行
    else
        if(from[x] ||(to[x] && tailer[x][to[x]]==pre[x] && cnt1[x]*2+cnt2[x]+cnt3[x]-1>0))
            pd[x]=0;//x作为终点
}
void clear()
{
    memset(head,0,sizeof(head));
    memset(Next,0,sizeof(Next));
    memset(from,0,sizeof(from));//该节点上的数字从哪条边进来
    memset(to,0,sizeof(to));//该节点上的数字从哪条边出去
    memset(cnt1,0,sizeof(cnt1));//该节点还剩下几条双向没走过的边
    memset(cnt2,0,sizeof(cnt2));//该节点还剩下几条单向出边
    memset(cnt3,0,sizeof(cnt3));//该节点还剩下几条双向出边
    memset(pd,0,sizeof(pd));//可行性
    memset(p,0,sizeof(p));//-1表示没走过,0表示双向都走过,x表示以x为起点单向走过这条边,这里的初始化好像没什么用
    memset(header,0,sizeof(header));//偏序链起始边
    memset(tailer,0,sizeof(tailer));//偏序链终边
    tot=0;
}
int main()
{
    scanf("%d",&T);
    while(T--)
    {
        clear();
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            scanf("%d",&rev[i]);
        for(int i=1,x,y;i<n;i++)
        {
            scanf("%d%d",&x,&y);
            add(x,y);
            cnt1[x]++,cnt1[y]++;
            p[x][y]=p[y][x]=-1;
            header[x][y]=tailer[x][y]=y,header[y][x]=tailer[y][x]=x;//初始值
        }
        for(int i=1,now;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
                pre[j]=0;
            pd[rev[i]]=1;//初始值
            check(rev[i],rev[i]);//dfs判断可行性
            for(int j=1;j<=n;j++)
                if(pd[j])
                {
                    now=j;
                    break;
                }//找到字典序最小的可行终点
            printf("%d ",now);
            from[now]=pre[now];
            while(pre[now]!=rev[i])
            {
                if(p[pre[now]][now]==-1)
                {
                    p[pre[now]][now]=p[now][pre[now]]=pre[now];
                    cnt1[now]--,cnt1[pre[now]]--,cnt3[now]++,cnt2[pre[now]]++;
                }//双向没走过
                else
                {
                    p[pre[now]][now]=p[now][pre[now]]=0;
                    cnt2[now]--,cnt3[pre[now]]--;
                }//反向走过
                header[pre[now]][tailer[pre[now]][now]]=header[pre[now]][pre[pre[now]]];
                tailer[pre[now]][header[pre[now]][pre[pre[now]]]]=tailer[pre[now]][now];//链表插入
                now=pre[now];
            }
            if(p[pre[now]][now]==-1)
            {
                p[pre[now]][now]=p[now][pre[now]]=pre[now];
                cnt1[now]--,cnt1[rev[i]]--,cnt3[now]++,cnt2[rev[i]]++;
            }
            else
            {
                p[pre[now]][now]=p[now][pre[now]]=0;
                cnt2[now]--,cnt3[rev[i]]--;
            }
            to[rev[i]]=now;
        }
        puts("");
    }
}

原文地址:https://www.cnblogs.com/TEoS/p/11969712.html

时间: 2024-10-08 16:40:55

题解【luogu5659 树上的数】的相关文章

CSP2019 树上的数 题解

题面 这是一道典型的部分分启发正解的题. 所以我们先来看两个部分分. Part 1 菊花图 这应该是除了暴力以外最好想的一档部分分了. 如上图(节点上的数字已省略),如果我们依次删去边(2)(1)(3)(4),那么操作完后2号点上的数字就会跑到1号点上,1号点数字会跑到3号点上,3号点数字跑到4号点上--依此累推.那么我们相当于把五个节点连成了一个环( 5 -> 2 -> 1 -> 3 -> 4 -> 5 ),每一个结点上的数字都会跑到环上的下一个结点上去,我们就是要求能使最

[题解]NKOJ 3102取数(乱搞)

题目描述 n个整数组成的一个环,现在要从中取出m个数,取走一个数字就不能取跟它相邻的数字(相邻的数不能同时取).要求取出的数字的总和尽可能大,问这个最大和是多少? 如果无解,请输出"Error!" 输入输出格式 输入格式: 第一行包含两个正整数n.m. 第二行为n个整数Ai. 输出格式: 仅一个整数,表示所求结果.如果无解输出"Error!",不包含引号. 输入输出样例 输入样例#1: 7 3 1 2 3 4 5 6 7 输出样例#1: 15 输入样例#2: 7 4

题解 P1036 【选数】

关于 P1036 [选数] 嗯,新手试炼场的,错了两次,对,我是蒟蒻. 因为这道题对我有帮助,所以,它是好题. 错啦两次,好尬的. 49--17--100: 不费话了,过程函数与递推. 当然要递推: 49分的不说了,从未先编译一下试试. 跟着题目走,判断质数. 来一段辣鸡代码 #include<bits/stdc++.h> using namespace std; int n,k; int x[25]; int ans; bool judge_prime(int x) { for(regist

题解 P3166 【[CQOI2014]数三角形】

蒟蒻的空间 做完之后看了看题解,怎么一篇和我思路一样的也没有...我好慌啊qwq(所以一定是窝太弱了看不懂dalao的思路) 好吧窝的方法确实很奇怪: 核心代码只有3行 输入 循环 输出 一气呵成 是题解中的豪杰 最重要的是 没有组合数 没有容斥 没有斜率 没有向量 DA☆ZE (只有我们的好朋友gcd 咳咳 那么开始正题(敲黑板) 首先,我们定义一个网格被一个三角形完全覆盖,当且仅当这个三角形的三个顶点都在网格边界上,并且沿着网格内部任意一条线段把网格切开,一定会把三角形切成两部分.比如下面的

【题解】CQOI2015选数

这题做的时候接连想错了好多次……但是回到正轨上之后依然是一个套路题.(不过这题好像有比莫比乌斯反演更好的做法,莫比乌斯反演貌似是某种能过的暴力ヽ(´ー`)┌)不过能过也就行了吧哈哈. 首先我们把数字的范围要进行缩小:最大公约数为 \(K\) 那自然所有选出来的数都必须是 \(K\) 的倍数.所以我们改选数为选择是 \(K\) 的多少倍.然后由于是最大公约数,所以选出来的这些数必须最大公约数等于\(1\).实际上多个数的最大公约数\( = 1\)完全可以和两个数的最大公约数 \( = 1\) 用一

题解 P1286 【两数之和】

提供一个新思路 这题,我们假设n个数分别为a1,a2,a3,a4,a5...an,且对于任意 1<=i<j<=n满足ai<aj 而他们两两之和即为输入的各数字,从中,我们不难推出对于输入的数字中(我们把它们按从小到大排序,分别设为m1,m2...) 一定满足:m1=a1+a2,m2=a1+a3(我们可以用反证法证明结论) 但m3有两种情况:m3=a1+a4或者m3=a2+a3,我们无法判断,那么, 为什么我们不能把a2+a3的情况排除呢? 所以我们可以这样做: 同样枚举a1(1-&

LeetCode题解001:两数之和

两数之和 题目 给定一个整数数组 nums?和一个目标值 target,请你在该数组中找出和为目标值的那?两个?整数,并返回他们的数组下标 你可以假设每种输入只会对应一个答案.但是,你不能重复利用这个数组中同样的元素 示例: 给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1] Java: 方法一:暴力法 暴力法很简单,就是用两遍循环的方式遍历nums class Solution {

Leetcode题解 - 双指针求n数之和

1. 两数之和 """ 双指针,题目需要返回下标,所以记录一个数字对应的下标 """ class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: r = [[ind, num] for ind, num in enumerate(nums)] r = sorted(r, key=lambda x: x[1]) head = 0 tail = len

题解 P1748 【H数】

我来讲讲 \(dp\) 的做法 前言 昨天 \(PHY\) 大佬问我,这题怎么做?考虑到他没学过 \(set\) . \(priority_queue\) 和 \(queue\) .之后,我就想到了可以用 \(dp\) 来解决这道题. 正文 设置状态 很显然,我们可以用 \(f[i]\) 表示第\(i\)个数是多少. 转移 第\(i\)个\(H\)数是多少,我们显然应该从前面的\(i-1\)个数去分别\(\times2\).\(\times3\).\(\times5\).\(\times7\)中