[2019CSP-S Day1]提高组Day1题解(格雷码[模拟(k转二进制取反的做法带证明)] + 括号树[DP] + 树上的数(暴力+菊花图+单链))

T1:格雷码

题目

通常,人们习惯将所有 n位二进制串按照字典序排列,例如所有2位二进制串按字典序从小到大排列为: 00, 01, 10, 11。
格雷码(Gray Code)是一种特殊的 n位二进制串排列法,它要求相邻的两个二进制串间恰好有一位不同,特别地,第一个串与最后一个串也算作相邻。
所有 2位二进制串按格雷码排列的一个例子为: 00, 01, 11, 10。
n 位格雷码不止一种,下面给出其中一种格雷码的生成算法:
1位格雷码由两个1 位二进制串组成,顺序为:0, 1。
n+1位格雷码的前2n个二进制串,可以由依此算法生成的 n 位格雷码(总共2n个 n 位二进制串)按顺序排列,再在每个串前加一个前缀 0 构成。
n + 1 位格雷码的后 2n个二进制串,可以由依此算法生成的 n 位格雷码(总共2n个 n 位二进制串)按逆序排列,再在每个串前加一个前缀 1 构成。
综上, n + 1 位格雷码,由 n 位格雷码的 2n个二进制串按顺序排列再加前缀 0,和按逆序排列再加前缀 1 构成,共2{n+1}个二进制串。另外,对于 n 位格雷码中的2n个二进制串,我们按上述算法得到的排列顺序将它们从0 ∼ 2n-1编号。
按该算法, 2 位格雷码可以这样推出:
已知 1 位格雷码为 0, 1。
前两个格雷码为 0 1, 0 1。后两个格雷码为 1 1, 1 0。合并得到 00, 01, 11, 10,编号依次为 0 ∼ 3。
同理, 3 位格雷码可以这样推出:
已知 2 位格雷码为: 00, 01, 11, 10。
前四个格雷码为:0 00, 0 01, 0 11, 0 10。后四个格雷码为:1 10, 1 11, 1 01,1 00。合并得到: 000, 001, 011, 010, 110, 111, 101, 100,编号依次为 0 ∼ 7。
现在给出 n, k,请你求出按上述算法生成的 n 位格雷码中的 k 号二进制串。
输入描述:
仅一行两个整数 n,k,意义见题目描述。
输出描述:
仅一行一个 n 位二进制串表示答案。
示例1
输入
2 3
输出
10
说明
2 位格雷码为:00,01,11,10,编号从 0∼3,因此 3 号串是 10
示例2
输入
3 5
输出
111
说明
3 位格雷码为:000,001,011,010,110,111,101,100,编号从 0∼7,因此 5 号串是 111。
备注:
对于 50% 的数据:n≤10
对于 80% 的数据:k≤5×106
对于 95% 的数据:k≤263−1
对于 100% 的数据:1≤n≤64 ,0≤k<2n

题解

额额额这道题就是一道模拟题,并不打算展开讲,这里的代码实现完全只是保存个代码罢了。这道题有很多方法实现,很多人的方法由于涉及到二的n次方带左移又没有开ULL会爆掉更多的分,这告诉我们在考场上要评估自己代码的危险性!!


看到格雷码的生成规律发现了,如果属于下半部分,它后面是与上半部分成镜面对称的,意思就是属于上半部分的从上往下第x个对应着下半部分的从下往上第x个,就当前那一位而言。。

所以我们可以把k转换成二进制,然后开始扫,遇到第一个1就输出1然后把k的当前位及其以后取个反,继续往右操作

代码实现

#include <cstdio>
#include <iostream>
using namespace std;
int n;
unsigned long long k;
int main() {
	scanf ( "%d", &n );
	cin >> k;
	for ( int i = n - 1;i >= 0;i -- ) {
		int flag = ( k >> i ) & 1;
		if ( flag ) printf ( "1" );
		else printf ( "0" );
		if ( flag ) k = ~k;
	}
	return 0;
}

可能会一脸蒙蔽为什么这么简单就可以,或者为什么是取反呢??接下来为大家带来比较简略的证明
在这里插入图片描述
假设n=3时,所有的情况从零开始编号依次如图所示,我们开始找规律,如果k的二进制前x个都是零,那么它就一直属于上半部分,我们观察得到,上半部分的规律就是对于y号位而言,把y-1号位的属于y号位的2(y-1)个数分成上下两半,都是上半部分是零,下半部分是1


如果出现了一个1,下半部分的规律就是对于y号位而言,把y-1号位的属于y号位的2(y-1)个数分成上下两半,上面都是1,下面都是0,直到出现了第二个1,才改变了它的规律变成上半部分是0,下半部分是1


所以能明白为什么要这么写了吗??其实跟我在考场上找到的规律是一样的,但是码力差距,就打出了天壤之别,接下来的就是我在考场时自己写的比较复杂的代码,可以跳过

#include <cstdio>
#include <iostream>
using namespace std;
#define LL unsigned long long
int n;
LL k;
LL pre[70];
void dfs1 ( int x, LL y );
void dfs2 ( int x, LL y );
 
void dfs1 ( int x, LL y ) {
    if ( x == 0 )
        return;
    if ( y >= pre[x - 1] ) {
        printf ( "0" );
        dfs1 ( x - 1, y - pre[x - 1] );
    }
    else {
        printf ( "1" );
        dfs2 ( x - 1, y );
    }
}
 
void dfs2 ( int x, LL y ) {
    if ( x == 0 )
        return;
    if ( y >= pre[x - 1] ) {
        printf ( "1" );
        dfs1 ( x - 1, y - pre[x - 1] );
    }
    else {
        printf ( "0" );
        dfs2 ( x - 1, y );
    }
}
 
int main() {
    scanf ( "%d", &n );
    cin >> k;
    pre[0] = 1;
    for ( int i = 1;i <= 64;i ++ )
        pre[i] = pre[i - 1] * 2;
    if ( k >= pre[n - 1] ) {
        printf ( "1" );
        dfs1 ( n - 1, k - pre[n - 1] );
    }
    else {
        printf ( "0" );
        dfs2 ( n - 1, k );
    }
    return 0;
}

T2:括号树

题目

本题中合法括号串的定义如下:
()是合法括号串。
如果 A 是合法括号串,则 (A) 是合法括号串。
如果 A, B是合法括号串,则 AB 是合法括号串。
本题中子串与不同的子串的定义如下:
字符串 S 的子串是 S 中连续的任意个字符组成的字符串。 S 的子串可用起始位置 l 与终止位置 r 来表示,记为 ( 1 ≤ l ≤ r ≤ ∣ S ∣ (1 \leq l \leq r \leq|S| 1lrS,∣S∣表示S的长度)。
S的两个子串视作不同当且仅当它们在S中的位置不同,即l不同或r不同。
【题目描述】
一个大小为 n 的树包含n个结点和n − 1 条边,每条边连接两个结点,且任意两个结点间有且仅有一条简单路径互相可达。
小 Q 是一个充满好奇心的小朋友,有一天他在上学的路上碰见了一个大小为 n 的树,树上结点从 1 ∼ n 编号, 1 号结点为树的根。除 1 号结点外,每个结点有一个父亲结点, u   ( 2 ≤ u ≤ n ) u \ (2 \leq u \leq n) u (2un)号结点的父亲为 f u   ( 1 ≤ f u < u ) f_{u} \ \left(1 \leq f_{u}<u\right) fu (1fu<u)号结点。
小 Q 发现这个树的每个结点上恰有一个括号,可能是’(‘或’)’。小Q定义 s i s_i si为:将根结点到 i 号结点的简单路径上的括号,按结点经过顺序依次排列组成的字符串。
显然 s i s_i si是个括号串,但不一定是合法括号串,因此现在小 Q 想对所有的 i   ( 1 ≤ i ≤ n ) i\ (1 \leq i \leq n) i (1in)求出, s i s_i si中有多少个互不相同的子串是合法括号串。
这个问题难倒了小 Q,他只好向你求助。设 s i s_i si 共有 k i k_i ki个不同子串是合法括号串,
你只需要告诉小 Q 所有 i × k i i \times k_{i} i×ki
的异或和,即:
( 1 × k 1 ) xor ⁡ ( 2 × k 2 ) xor ⁡ ( 3 × k 3 )  xor  ⋯  xor  ( n × k n ) \left(1 \times k_{1}\right) \operatorname{xor}\left(2 \times k_{2}\right) \operatorname{xor}\left(3 \times k_{3}\right) \text { xor } \cdots \text { xor }\left(n \times k_{n}\right) (1×k1)xor(2×k2)xor(3×k3) xor  xor (n×kn)
其中 xor 是位异或运算。
输入描述:
第一行一个整数 n,表示树的大小。
第二行一个长为 n 的由’(’ 与’)’ 组成的括号串,第 i 个括号表示 i 号结点上的括号。
第三行包含 n− 1 个整数,第i (1≤i<n)个整数表示 i + 1 号结点的父亲编号 f i + 1 f_{i+1} fi+1
输出描述:
仅一行一个整数表示答案。
示例1
输入
5
(()()
1 1 2 2
输出
6
说明
树的形态如下图:
在这里插入图片描述
将根到 1 号结点的简单路径上的括号,按经过顺序排列所组成的字符串为 (,子串 是合法括号串的个数为 0。
将根到 2 号结点的简单路径上的括号,按经过顺序排列所组成的字符串为 ((,子 串是合法括号串的个数为 0。
将根到 3 号结点的简单路径上的括号,按经过顺序排列所组成的字符串为 (),子 串是合法括号串的个数为 1。
将根到 4 号结点的简单路径上的括号,按经过顺序排列所组成的字符串为 (((,子 串是合法括号串的个数为 0。
将根到 5 号结点的简单路径上的括号,按经过顺序排列所组成的字符串为 ((),子 串是合法括号串的个数为 1。
在这里插入图片描述

题解

这里不再单独呈现单链的暴力分,因为我们发现单链的思路可以完全适用于整棵树上

思考如果x这个点上的括号是右括号,那么它可能对答案进行贡献的前提就是从根到x这单独的一条链上前面至少还有一个多余的左括号,跟它进行匹配,那么它的值就会从前一个未能匹配的左括号的值转移+1。并且这个左括号一定是最远离根就是最接近于x的位置

那么我们就设 d p [ i ] dp[i] dp[i]表示从根1到i位置为止的匹配成功的括号数,不管匹配成功的括号里面又有多少个成功的括号对,例如 ( ( ) ( ) ) (()()) (()())到最后一个右括号时它的dp只有1
那么这中间多多搭配多出的成功匹配对就通过dfs往下传递就可以了

p r e [ i ] pre[i] pre[i]表示与i无法匹配的距离i最近的一个左括号所在的位置


如果i本身是一个左括号就不可能与前面匹配成功,它的dp就是0,并且离它最近的左括号就是自己本身嘛!!
如果i是右括号,那么就要再次判断到它爸爸为止之前是否有多余的左括号与i匹配
如果有那么i就与记录下的它爸爸的最近的一个无法匹配的左括号位置进行匹配,即
p r e [ f [ i ] ] pre[f[i]] pre[f[i]],那么它的括号匹配对就是与i匹配的左括号的父亲为止的匹配对再加1
那么转移方程式就出来了 d p [ u ] = d p [ f [ p r e [ f [ i ] ] ] ] + 1 dp[u]=dp[f[pre[f[i]]]]+1 dp[u]=dp[f[pre[f[i]]]]+1

那么接下来就要转移 p r e [ i ] pre[i] pre[i],我们想一想如果i与某一个左括号匹配成功了,那么距离i最近的一个多余的左括号其实就是匹配成功的左括号的父亲所无法匹配的最近的左括号,转移方程式如下 p r e [ i ] = p r e [ f [ p r e [ f [ i ] ] ] ] pre[i]=pre[f[pre[f[i]]]] pre[i]=pre[f[pre[f[i]]]]

代码实现

#include <cstdio>
#include <vector>
using namespace std;
#define LL long long
#define MAXN 500005
vector < int > G[MAXN];
int n;
LL result;
int f[MAXN], tree[MAXN];
LL dp[MAXN], pre[MAXN];

void dfs ( int u, LL sum ) {
	if ( tree[u] == '(' )
		pre[u] = u;
	else if ( f[u] && pre[f[u]] ) {
		dp[u] = dp[f[pre[f[u]]]] + 1;
		pre[u] = pre[f[pre[f[u]]]];
	}
	sum += dp[u];
	result ^= sum * u; 
	for ( int i = 0;i < G[u].size();i ++ ) {
		int v = G[u][i];
		dfs ( v, sum );
	}
}

int main() {
	scanf ( "%d\n", &n );
	for ( int i = 1;i <= n;i ++ )
		scanf ( "%c", &tree[i] );
	for ( int i = 2;i <= n;i ++ ) {
		scanf ( "%d", &f[i] );
		G[f[i]].push_back( i );
	}
	dfs ( 1, 0 );
	printf ( "%lld", result );
	return 0;
} 

T3:树上的数

题目

给定一个大小为 n 的树,它共有 n 个结点与 n − 1 条边,结点从 1 ∼ n 编号。初始时每个结点上都有一个 1 ∼ n 的数字,且每个 1 ∼ n 的数字都只在恰好一个结点上出现。
接下来你需要进行恰好n − 1 次删边操作,每次操作你需要选一条未被删去的边,此时这条边所连接的两个结点上的数字将会交换,然后这条边将被删去。
n − 1 次操作过后,所有的边都将被删去。此时,按数字从小到大的顺序,将数字1 ∼ n 所在的结点编号依次排列,就得到一个结点编号的排列 P i P_i Pi 。现在请你求出,在最优操作方案下能得到的字典序最小的 P i P_i Pi
在这里插入图片描述
如上图,蓝圈中的数字 1 ∼ 5 一开始分别在结点 ② 、① 、③ 、⑤ 、④ 。按照 (1)(4)(3)(2)的顺序删去所有边,树变为下图。按数字顺序得到的结点编号排列为 ① ③ ④ ② ⑤ ,该排列是所有可能的结果中字典序最小的。
在这里插入图片描述
输入描述:
本题输 入包含多组测试数据。
第一行一个正整数 T,表示数据组数。
对于每组测试数据:
第一行一个整数 n,表示树的大小。
第二行 n 个整数,第 i ( 1 ≤ i ≤ n ) i(1\leq i\leq n) i1in个整数表示数字 i 初始时所在的结点编号。 接下来 n−1 行每行两个整数 x,y,表示一条连接 x 号结点与 y 号结点的边
输出描述:
对于每组测试数据,输出一行共 n 个用空格隔开的整数,表示最优操作方案下所 能得到的字典序最小的 P i P_i Pi
示例1
输入
4
5
2 1 3 5 4
1 3
1 4
2 4
4 5
5
3 4 2 1 5
1 2
2 3
3 4
4 5
5
1 2 5 3 4
1 2
1 3
1 4
1 5
10
1 2 3 4 5 7 8 9 10 6
1 2
1 3
1 4
1 5
5 6
6 7
7 8
8 9
9 10
输出
1 3 4 2 5
1 3 5 2 4
2 3 1 4 5
2 3 4 5 6 1 7 8 9 10
在这里插入图片描述
对于所有测试点: 1 ≤ T ≤ 10 1\leq T \leq10 1T10,保证给出的是一个树

10pts暴力题解

暴力题解的话十分简单,因为n为10,我们就跑出每一种可能的删边顺序,一共是 ! ( n − 1 ) !(n-1) !(n1)再加上有T组数据最多也就是10,极限时间复杂度就是 O ( ! n ) O(!n) O(!n),对于10而言是可以卡过去的

代码实现

#include <cstdio>
#include <iostream>
using namespace std;
#define MAXN 2005
struct node {
    int u, v;
    node () {}
    node ( int U, int V ) {
        u = U;
        v = V;
    }
}edge[MAXN];
int T, n;
int num[MAXN], tree[MAXN], a[MAXN], b[MAXN], c[MAXN];
bool vis[MAXN];
string result;
 
void dfs ( int x ) {
    if ( x == n ) {
        string str = "";
        for ( int i = 1;i <= n;i ++ ) {
            b[i] = tree[i];
            c[tree[i]] = i;
        }
        for ( int i = 1;i < n;i ++ )
            swap ( c[b[edge[a[i]].u]], c[b[edge[a[i]].v]] );
        for ( int i = 1;i <= n;i ++ )
            str += ( c[i] + '0' );
        if ( result == "" )
            result = str;
        else
            result = min ( result, str );
        return;
    }
    for ( int i = 1;i < n;i ++ )
        if ( ! vis[i] ) {
            vis[i] = 1;
            a[x] = i;
            dfs ( x + 1 );
            vis[i] = 0;
        }
}
 
int main() {
    scanf ( "%d", &T );
    while ( T -- ) {
        scanf ( "%d", &n );
        for ( int i = 1;i <= n;i ++ ) {
            scanf ( "%d", &num[i] );
            tree[num[i]] = i;
        }
        for ( int i = 1;i < n;i ++ ) {
            int x, y;
            scanf ( "%d %d", &x, &y );
            edge[i] = ( node ( x, y ) );
        }
        if ( n <= 10 ) {
            result = "";
            dfs ( 1 );
            for ( int i = 0;i < n;i ++ )
                printf ( "%d ", result[i] - '0' );
            printf ( "\n" );
        }
    }
    return 0;
}

25pts菊花图题解

对于菊花图也就是长成如下的样子,数据范围的解释也给的很明确就算考场上不知道什么是菊花图也应该画得出来这种图
在这里插入图片描述
都能想到肯定是依次满足1,2,3…的要求,尽量把1送到1节点,这样才能保证字典序最小
其实根根本没有什么特殊作用,它就是起一个中转站的感觉,两个叶子进行交换的时候路过了罢了
对于菊花图有三种移动方式
1、根移动到某一个叶子节点
2、某一个叶子节点移动到根,那么就必须要求这一条边是所有边中最后一条被删掉的
3、两个叶子之间的移动
我们考虑有哪种情况是不合法的移动,很显然
1、如果根想移动到某一个叶子节点,而那个叶子节点也想移动到根,这就要求这条边是最后一条被删的边,并且删完其他边后,根上面的值没有动过,肯定是满足不了的!!!
2、两个叶子之间想相互转移也是实现不了的,只能满足其中一个
所以我们就要求转移后删的边形成一个环,怎么做呢??并查集!!
如果i与j本身就是一个集合,就不能把j连向i,这样会提前形成环

这种情况连边都可以直接不管了,比暴力还来得容易

代码实现

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
#define MAXN 2005
int T, n;
int num[MAXN], tree[MAXN];
int f[MAXN], ans[MAXN];
bool vis[MAXN];

void makeSet () {
    for ( int i = 1;i <= n;i ++ )
        f[i] = i;
}
int findSet ( int x ) {
    if ( x != f[x] )
        f[x] = findSet ( f[x] );
    return f[x];
}
 
int main() {
    scanf ( "%d", &T );
    while ( T -- ) {
        scanf ( "%d", &n );
        for ( int i = 1;i <= n;i ++ ) {
            vis[i] = 0;
            scanf ( "%d", &num[i] );
            tree[num[i]] = i;
        }
        for ( int i = 1;i < n;i ++ ) {
            int x, y;
            scanf ( "%d %d", &x, &y );
        }
        makeSet();
        for ( int i = 1;i < n;i ++ ) {
            int now = 0x7f7f7f7f;
            for ( int j = 1;j <= n;j ++ ) {
                if ( ! vis[j] && findSet ( j ) != num[i] ) {
                    now = min ( now, j );
            	}
            }
            ans[i] = now;
            vis[now] = 1;
            f[num[i]] = now;
        }
        for ( int i = 1;i < n;i ++ )
            printf ( "%d ", ans[i] );
        for ( int i = 1;i <= n;i ++ )
            if ( ! vis[i] ) {
                printf ( "%d\n", i );
                break;
            }
    }
    return 0;
}

25pts单链题解

单链的话,也比较好想,实现也。。。
借用菊花图的思想,肯定也是1,2,3…这样依次满足下去,但是单链就意味着他们是在同一条路上进行操作,就会有不同的方向,对于一个链上的点(除开头和尾),会有两条边,删边顺序不是先左后右就是先右后左,我们用1表示先右后左,2表示先左后右,0表示没限制都可以

对于接下来的操作,我们思考如果点x要往左转移到y,那么这一路上要满足的性质就是,对于x这个点就必须先左后右地转移,否则x可能往右转移走了,y就必须先左后右,这样当最后一次交换y与y的右边的时候,x就可以被锁死在y而不被转移掉,这一路上的点都必须为0无限制或者1,这样从右往左才能依次像过安检一样把x传过去。。。对于一个点右移与上面的思路是一样的这里不再赘述

最后单独处理一下头和尾就不对他两个进行特殊打标,反正都只有一条边就没有所谓的先后关系

代码实现

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
#define MAXN 2005
vector < int > g[MAXN];
int T, n;
int num[MAXN], tree[MAXN];
int vis[MAXN];
int d[MAXN], f[MAXN], ans[MAXN], pus[MAXN];
  
void build ( int u, int x, int fa ) {
    pus[x] = u;
    num[tree[u]] = x;
    for ( int i = 0;i < g[u].size();i ++ ) {
        int v = g[u][i];
        if ( v == fa )
            continue;
        build ( v, x + 1, u );
    }
}
  
int main() {
    scanf ( "%d", &T );
    while ( T -- ) {
        scanf ( "%d", &n );
        for ( int i = 1;i <= n;i ++ ) {
            d[i] = 0;
            vis[i] = 0;
            pus[i] = 0;
            g[i].clear();
            scanf ( "%d", &num[i] );
            tree[num[i]] = i;
        }
        for ( int i = 1;i < n;i ++ ) {
            int x, y;
            scanf ( "%d %d", &x, &y );
            d[x] ++;
            d[y] ++;
            g[x].push_back( y );
            g[y].push_back( x );
        }
        int root;
        for ( int i = n;i >= 1;i -- )
            if ( d[i] == 1 )
                root = i;
		build ( root, 1, 0 );
        for ( int i = 1;i <= n;i ++ ) {
            int now = 0x7f7f7f7f, idx;
            if ( ! vis[num[i]] || vis[num[i]] == 2 ) {
                for ( int j = num[i] - 1;j >= 1;j -- ) {
                    if ( ! vis[j] || vis[j] == 2 ) {
                        if ( pus[j] < now ) {
                            now = pus[j];
                            idx = j;
                        }
                    }
                    if ( vis[j] == 2 )
                        break;
                }
            }
            if ( ! vis[num[i]] || vis[num[i]] == 1 ) {
                for ( int j = num[i] + 1;j <= n;j ++ ) {
                    if ( ! vis[j] || vis[j] == 1 ) {
                        if ( pus[j] < now ) {
                            now = pus[j];
                            idx = j;
                        }
                    }
                    if ( vis[j] == 1 )
                        break;
                }
            }
            ans[i] = now;
            if ( idx < num[i] ) {
                if ( num[i] != 1 && num[i] != n )
                    vis[num[i]] = 2;
                if ( idx != 1 && idx != n )
                	vis[idx] = 2;
                for ( int j = idx + 1;j < num[i];j ++ )
                    vis[j] = 1;
            }
            else {
                if ( num[i] != 1 && num[i] != n )
                    vis[num[i]] = 1;
                if ( idx != 1 && idx != n )
                    vis[idx] = 1;
                for ( int j = idx - 1;j > num[i];j -- )
                    vis[j] = 2;
            }
        }
        for ( int i = 1;i < n;i ++ )
            printf ( "%d ", ans[i] );
        printf ( "%d\n", ans[n] );
        continue;
    }
    return 0;
}

把这三个部分的代码进行整合,就能达到60的高分了
在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值