2022牛客寒假算法基础集训营3


比赛链接

2022牛客寒假算法基础集训营3

G.智乃的树旋转(easy version)

题目描述

本题有对应的hard version, easy version有额外的限制条件,保证easy version的测试用例集是hard version的子集。
树旋转是在二叉树中的一种子树调整操作, 每一次旋转并不影响对该二叉树进行中序遍历的结果。
树旋转通常应用于需要调整树的局部平衡性的场合。树旋转包括两个不同的方式,分别是左旋和右旋。 两种旋转呈镜像,而且互为逆操作。

如图所示,为树的左旋转和右旋转示意图。当\({A}\)节点位于\({B}\)节点的左子树时,可以进行以\({A}\)节点为旋转轴的右旋转,反之可以进行以\({B}\)节点为旋转轴的左旋转。

具体来讲

右旋转:
当节点\({A}\)位于节点\({B}\)的左孩子时,节点\({A}\)可作为旋转轴进行右旋转,该旋转操作涉及到结构改变的节点有\({4}\)个。
我们记旋转前\({A}\)节点的右孩子为\({\beta}\)节点,\({B}\)节点的父节点为\({X}\)节点(若\({B}\)节点就是整棵树的根节点,则无需做与\({X}\)节点相关的操作)。
则旋转后\({\beta}\)节点由\({A}\)节点的右孩子变为\({B}\)节点的左孩子,\({X}\)节点的其中一个孩子节点由\({B}\)节点改变为\({A}\)节点,并且它们和\({X}\)节点的中序顺序不发生其他变化;也就是说原来\({B}\)节点是\({X}\)节点的右孩子,则旋转后变为\({A}\)节点替换\({B}\)节点成为\({X}\)节点新的右孩子,反之若\({B}\)节点初始是\({X}\)节点的左孩子,则旋转后\({A}\)节点替换{B}B节点成为\({X}\)节点新的左孩子。

左旋转:
当节点\({B}\)位于节点\({A}\)的右孩子时,节点\({B}\)可作为旋转轴进行左旋转,该旋转操作涉及到结构改变的节点有\({4}\)个。
我们记旋转前\({B}\)节点的左孩子为\({\beta}\)节点,\({A}\)节点的父节点为\({X}\)节点(若\({A}\)节点就是整棵树的根节点,则无需做与\({X}\)节点相关的操作)。
则旋转后\({\beta}\)节点由\({B}\)节点的左孩子变为\({A}\)节点的右孩子,\({X}\)节点的其中一个孩子节点由\({A}\)节点改变为\({B}\)节点,并且它们和\({X}\)节点的中序顺序不发生其他变化;也就是说原来\({A}\)节点是\({X}\)节点的右孩子,则旋转后变为\({B}\)节点替换\({A}\)节点成为\({X}\)节点新的右孩子,反之若\({A}\)节点初始是\({X}\)节点的左孩子,则旋转后\({B}\)节点替换\({A}\)节点成为\({X}\)节点新的左孩子。

智乃最近学习了树旋转,树旋转的本质是二叉树旋转轴节点与其父节点父子关系的改变,从视觉效果上看起来好像整个树进行了“旋转”。
在实际的操作过程中,如果指定旋转轴,其实就没有所谓“左旋转”和“右旋转”的区别,当旋转轴确定时,将只有一种旋转方法,遵循以下规则。
根节点无法作为旋转轴被选定。
当旋转轴是其父节点的左子树时,旋转轴只能进行右旋转。
当旋转轴是其父节点的右子树时,旋转轴只能进行左旋转。
现在智乃有一颗尺寸大小为{N}N二叉树,智乃对其做了一次旋转操作将其打乱,她想让你通过一系列树的旋转操作将其还原,要求你的操作次数不多于\({N^2}\)次。

输入描述:

第一行输入一个正整数\({N(1 \le N \le 10^3) }\),表示二叉树的节点数目,节点从\({1}\)\({N}\)标号。

接下来\({N}\)行输入二叉树一开始的样子。
每行输入两个非负整数\({lch,rch(0\le lch,rch \le N)}\)表示每个节点的左右子树。
\({lch}\)的值为\({0}\)时,则表示该节点无左子树,当\({rch}\)的值为\({0}\)时,则表示该节点无右子树。

接下来\({N}\)行输入二叉树被打乱后的样子。
每行输入两个非负整数\({lch,rch(0\le lch,rch \le N)}\)表示每个节点的左右子树。
\({lch}\)的值为\({0}\)时,则表示该节点无左子树,当\({rch}\)的值为\({0}\)时,则表示该节点无右子树。

要求你将打乱后的二叉树通过一系列旋转操作还原

输出描述:

首先输出一个整数\({K}\),表示你还原二叉树进行旋转的次数,要求你给出\({K}\)的范围在\([0,N^2]\),接下来\({K}\)行,依次输出旋转操作的旋转轴。
由于旋转轴只能进行左旋转或者右旋转其中的一种,裁判程序将会自己判断当前需要进行的是左旋转还是右旋转。
注意:旋转过程中的根节点无法作为旋转轴进行旋转,如果你指定旋转过程中的根节点作为旋转轴,则裁判程序将直接给出WA的结果。

easy version额外限制条件,保证可以通过不多于一次树旋转的操作还原二叉树,即一定存在\({K\leq1}\)的解。

输入

5
2 3
0 0
4 5
0 0
0 0
2 4
0 0
1 5
0 0
0 0

输出

1
1

输入

1
0 0
0 0

输出

0

解题思路

模拟

旋转对对换两个节点的父子关系,对换后原来的父节点变为儿子节点,所以需要找到原来的父节点将其旋转恢复,可以记录每个结点的父节点,然后遍历每个结点,判断与其父节点是否满足;fa2[fa1[i]]==i,即原来的父节点转化为儿子节点后其父节点为原来父节点的儿子节点

  • 时间复杂度:\(O(n)\)

代码

// Problem: 智乃的树旋转(easy version)
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/23478/G
// Memory Limit: 524288 MB
// Time Limit: 2000 ms

// %%%Skyqwq
#include 

#define pb push_back
#define fi first
#define se second
#define mp make_pair
using namespace std;

typedef long long LL;
typedef pair PII;

template  bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template  bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }

template  void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}

int fa1[1005],fa2[1005],n;
int main()
{
	scanf("%d",&n);
	int l,r;
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&l,&r);
		fa1[l]=fa1[r]=i;
	}
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&l,&r);
		fa2[l]=fa2[r]=i;
	}
	for(int i=1;i<=n;i++)
		if(fa1[i]&&fa2[fa1[i]]==i)
		{
			printf("1\n%d",fa1[i]);
			return 0;
		}
	puts("0");
	return 0;
}

I.智乃的密码

题目描述

智乃去注册账号,他发现网站的的密码必须符合以下几个条件

  • 密码是仅包含大小写英文字母、数字、特殊符号的字符串。
  • 密码的长度不少于\({L}\)个字符,并且不多于\({R}\)个字符。
  • 密码中应该至少包括①大写英文字母、②小写英文字母、③数字、④特殊符号这四类字符中的三种。
    所谓特殊字符,是指非大小写字母、数字以及空格回车等不可见字符的可见字符,包括但不限于"~!@#$%^&*()_+"。

现在智乃有一个长度大小为\({N}\)的字符串\({S}\),她想知道{S}S串中有多少个子串是一个符合条件的密码,请你帮助智乃统计符合条件的密码数目。
子串是指字符串中某一段连续的区间,例如对于字符串"abcde"来说,"abc","cde"都是它的子串,而"ace"不是它的子串。

输入描述:

第一行输入三个正整数\({N,L,R(1\le N \le 10^5,1\le L\le R \le N)}\),表示{S}S串的长度,合法密码长度应该在\({L}\)\({R}\)个字符之间。
接下来一行输入一个长度为\({N}\)的字符串\({S}\),字符串仅包括①大写英文字母、②小写英文字母、③数字、④特殊符号四类字符。

输出描述:

仅一行一个整数,表示有多少子串是一个合法的密码。

输入

10 6 8
asdfeg111*

输出

3

说明

"eg111","feg111","dfeg111*"

解题思路

双指针

先构建一个 \(L\) 长度的滑动窗口,即动态维护考虑左右两个指针,计算每个进入窗口的字符对答案的贡献,找到满足长度为 \(L\) 的限制的同时找到满足至少有三种字符的最靠右的位置,该位置与左指针的距离即为该字符的答案的贡献,另外当窗口过长时要移动左指针

  • 时间复杂度:\(O(n)\)

代码

// Problem: 智乃的密码
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/23478/I
// Memory Limit: 524288 MB
// Time Limit: 2000 ms

// %%%Skyqwq
#include 

#define pb push_back
#define fi first
#define se second
#define mp make_pair
using namespace std;

typedef long long LL;
typedef pair PII;

template  bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template  bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }

template  void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}

const int N=1e5+5;
int n,L,R,Left[4][N];
char s[N];
inline int get(char &c)
{
	if(c>='A'&&c<='Z')return 0;
	if(c>='a'&&c<='z')return 1;
	if(c>='0'&&c<='9')return 2;
	return 3;
}
int main()
{
	scanf("%d%d%d",&n,&L,&R);
	scanf("%s",s+1);
	int lst[4]={0};
	for(int i=1;i<=n;i++)
	{
		if(get(s[i])==0)lst[0]=i;
		if(get(s[i])==1)lst[1]=i;
		if(get(s[i])==2)lst[2]=i;
		if(get(s[i])==3)lst[3]=i;
		Left[0][i]=lst[0];
		Left[1][i]=lst[1];
		Left[2][i]=lst[2];
		Left[3][i]=lst[3];
	}
	LL res=0;
	for(int r=L,l=1;r<=n;r++)
	{
		if(r-l+1>R)l++;
		int a[4]={Left[0][r],Left[1][r],Left[2][r],Left[3][r]};
		sort(a,a+4);
		if(a[1]

L.智乃的数据库

题目描述

智乃最近在学习数据库的查询语言SQL。SQL (Structured Query Language:结构化查询语言) 是用于管理关系数据库管理系统(RDBMS)。 SQL 的范围包括数据插入、查询、更新和删除,数据库模式创建和修改,以及数据访问控制。

使用SQL,可以灵活的对数据库表进行操作,在数据库的查询语句中有"GROUP BY"这样一种关键字。
GROUP BY 语句用于结合聚合函数,根据一个或多个列对结果集进行分组。

例如数据库中储存了这样一个数据库表Table,执行带有"GROUP BY"关键字的查询语句。

id name val
1 chino 1
2 chino 2
3 kokoa 3
SELECT COUNT(*) FROM Table GROUP BY name;

SQL语句中COUNT表示聚合后每一个组中有多少条数据。
当执行上述的SQL查询语句后,其返回的结果如下

2
2 1

第一行的2表示按照name字段进行分组聚合,一共可以分出2组。
第二行的两个整数表示每组中各有多少条数据。

当然了,"GROUP BY"关键字是可以选中多个列进行分组聚合的,只需要把这些字段用逗号隔开即可。

SELECT COUNT(*) FROM Table GROUP BY name,val;

例如这样的SQL查询语句,执行后返回的结果如下

3
1 1 1

现在智乃把这张数据库表导出给你,请你执行一个SELECT COUNT(*) FROM Table GROUP BY ...;的查询语句,请你把查询的结果告诉智乃。
为了简化问题,我们假设数据库的表由\(N\)条记录以及\(M\)个字段组成,且这些字段的类型均为int类型。

输入描述:

第一行输入两个正整数\({N,M(1\leq N,M\leq1000)}\),表示数据库表中有4N\(条记录以及\)M$个字段。

接下来一行输入MM个字符串\(S_i (1\leq|S_i|\leq50)\)表示每个字段的名称。

接下来\(N\)行,每行\(M\)列,输入一个二维表格\(data_{i,j} (0\leq data_{i,j} \leq 10^9)\)表示数据库表中第\({i}\)条记录的第\({j}\)个字段的值。

接下来输入一行一个字符串SQL\((1\leq |SQL|\leq5\times10^4)\),表示查询语句,查询语句的格式为"SELECT;;COUNT(*);;FROM;;Table;;GROUP;;BY;;...; ""SELECTCOUNT(?)FROMTableGROUPBY...;"。

其中"...""..."是若干个字段名称,保证是之前输入的MM个字段中的某些字段,且这若干个字段互不相同,字段与字段之间用一个逗号隔开。

输出描述:

第一行输出一个整数\(K\)表示聚合后组的数目。
接下来一行输出\(K\)个整数,表示每组中聚合的记录数目,整数与整数之间用空格隔开。

你可以按照你喜欢的顺序输出答案,裁判程序将忽略输出顺序的影响。

输入

4 3
id name val
1 1 2
1 1 3
1 2 1
2 2 2
SELECT COUNT(*) FROM Table GROUP BY name,id;

输出

3
1 2 1

说明

你可以按照自己喜欢的顺序输出
例如

3
2 1 1

则也能得到正确的结果。

解题思路

哈希,区间问题

先将字段提取出来并转换为列号,然后将每行的对应列用一个数值表示出来,即哈希值,然后将这些哈希值排序,进而转化为求区间相同段数问题

  • 时间复杂度:\(O(nmlog(nm))\)

代码

// Problem: 智乃的数据库
// Contest: NowCoder
// URL: https://ac.nowcoder.com/acm/contest/23478/L
// Memory Limit: 524288 MB
// Time Limit: 2000 ms

// %%%Skyqwq
#include 

#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;

typedef long long LL;
typedef pair PII;

template  bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template  bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }

template  void inline read(T &x) {
    int f = 1; x = 0; char s = getchar();
    while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
    while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
    x *= f;
}

const int N=1e3+5,mod=1e9+7;
unordered_mapmp;
int n,m,a[N][N];
int t;
string s;
vector b;
inline int get(int x)
{
	int res=1;
	for(int &y:b)
	{
		res=(1ll*res*131+a[x][y])%mod;
	}
	return res;
}
int main()
{
	cin>>n>>m;
	for(int i=0;i>s;
		mp[s]=++t;
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)cin>>a[i][j];
	cin.ignore();
	getline(cin,s);
	int pos=s.find("BY ")+3;
	int pos1=s.find(',');
	if(pos1==-1)
		b.push_back(mp[s.substr(pos,s.size()-2-pos+1)]);
	else
	{
		b.push_back(mp[s.substr(pos,pos1-1-pos+1)]);
		while(s.find(',',pos1+1)!=-1)
		{
			int t=pos1;
			pos1=s.find(',',pos1+1);
			b.push_back(mp[s.substr(t+1,pos1-1-(t+1)+1)]);
		}
		b.push_back(mp[s.substr(pos1+1,s.size()-2-(pos1+1)+1)]);
	}
	vector c;
	for(int i=1;i<=n;i++)c.push_back(get(i));
	sort(c.begin(),c.end());
	int num=0;
	vector ans;
	int st=c[0],num1=1;
	n=c.size();
	for(int i=1;i