牛客xiao白月赛62-D,E,F

 这次小白赛好难啊,我还是太菜了,最后三题完全不会。。ort...

D.子树的大小(思维)

链接:https://ac.nowcoder.com/acm/contest/47266/D
来源:牛客网
 

题目描述

牛牛有一颗包含 n 个结点的 k 叉树,这些结点编号为 0…n−1 。

定义一颗 k 叉树:
    1、以结点 0 为根。
    2、编号为 x 结点的 k 个儿子编号分别为: k×x+1…k×x+k。

牛妹有 m 个询问表示为:q1​,q2​…qm​。
 

对于第 i 个询问,你需要告诉牛妹编号为qi​ 的结点,其的子树中结点的个数(含结点 qi​)。

输入描述:

本题采用多组案例输入,第一行一个整数 T 代表案例组数。
每组案例中,第一行包含三个空格分隔的整数:n k m。
接下来一行包含 m 个空格分隔的整数代表:q1​,q2​…qm​。
保证:
0<n≤1e16
0<k≤100
0<m≤1e5
0≤qi​<n

单个测试点中所有案例 m 的和不超过 3×1e5

输出描述:

对于每组案例,输出共 m 行,每行一个整数代表答案。

示例1

输入

2
9 3 5
0 8 2 1 3
1 1 1
0

输出

9
1 
3
4
1
1

 该题简单的思维题 (我比赛时竟然没想到ort...),根据观察可以发现,以q为根节点的子树,其每层最左边的节点的编号l等于上一层最左边节点的编号*k+1,其最右边的节点的编号r等于上一层最右边的节点编号*k+k, 而该层节点个数则等于r-l+1,所以可以dfs遍历每一层,然后用个变量记录该层节点个数。

到遍历到最后时,有两种情况,一种是该层的r要大于n-1(因为n是个数,所以n个节点最后的节点编号是n-1),这时该层节点个数为n-1-l+1; 另一种情况是该层的l要大于n-1,这时表示n-1号节点不在以q为根节点的子树内,所以舍弃这一层节点,直接返回。 

要注意的是,k等于1时,是单叉树,遍历的次数可能会到1e16的级别,会超时,所以需要特判。 

 代码如下:

#include<iostream>
using namespace std;
typedef long long LL;
LL n, k, m, ans = 0; 
void dfs(LL l, LL r)
{
	if(l>n-1) return ; //l大于最后一个节点编号,直接返回 

	if(r>n-1)
	{
		ans += (n-1-l+1); 
		return ; //这里也可以不返回,当继续向下遍历时,上面if(l>n-1)的判断也会让它返回 
	}
	ans += (r-l+1);
	dfs(l*k+1, r*k+k);
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int t;
	cin >> t;
	while(t --)
	{
		cin >> n >> k >> m;
		while(m --)
		{
			LL q;
			cin >> q;
			if(k==1) cout << (n-1-q+1) << endl; //特判1的情况 
			else
			{
				ans = 0;
				dfs(q, q); //开始的最左节点和最右节点,就是它本身 
				cout << ans << endl;
			}
		}
	}
	return 0;
}

E.剖分(树上差分) 

链接:https://ac.nowcoder.com/acm/contest/47266/E
来源:牛客网
 

题目描述

牛牛有一颗包含 n 个结点的二叉树,这些结点编号为 1…n 。这颗树被定义为:
    1、以结点 1 为根。
    2、编号为 x 结点的两个儿子编号分别为: 2×x 和 2×x+1。
    3、每个结点的权重初始都为 0。

牛牛接下来会对这颗树进行 m 次操作,操作的格式是以下四种之一:
    1、op x (这里 op=1 )代表牛牛将以编号 x 为根结点的子树中所有结点的权重 +1。
    2、op x (这里 op=2 )代表牛牛将以编号 x 为根结点的子树外的所有结点权重 +1。
    3、op x (这里 op=3 )代表牛牛将根结点到编号 x 结点的路径上的所有结点权重 +1。
    4、op x (这里 op=4 )代表牛牛将根节点到编号 x 结点的路径上的结点之外的所有结点权重 +1。

牛妹想知道当牛牛的所有操作结束之后,树中权重为 0,1,2…m 的结点的数量分别是多少。

输入描述:

 
 

第一行输入两个空格分隔的整数:n m。
接下来 m 行,每行描述了一个牛牛进行的操作。
保证:
0<n≤1e7
0<m≤5×1e5
0<op≤4
0<x≤n

输出描述:

输出一行共 m+1 个整数,代表答案。

示例1

输入

7 4
1 2
3 5
4 3
2 7

输出

0 2 2 1 2

该题用树上差分写。 这里用了两种差分方式,第一种差分方式是向下更新,让每个编号为i的节点的权值等于其本身加上根节点的权值,即val[i] += val[i/2]。第二种差分方式是向上更新,让每个编号为i的节点的权值等于其本身加上子节点的权值,即val[i] += val[i*2]+val[i*2+1];

由于操作1,2与以编号为x的根节点的子树有关,所以更适合第一种差分方式,向下更新。 

而操作3,4与整棵树的根节点到x节点的路径有关,所以更适合第二种差分方式,向上更新。(因为每次更新时不用考虑分支的问题,只需对x更新即可。用第一种差分方式,还得对该路径上分支的节点更新)

对于操作2和4的除指定部分的加一,只要对根节点加一(相当于对整棵树的所有节点加一),然后以相应的差分方式对指点部分的节点减一即可。 

所以设向下更新的差分数组dt,向上更新的ut,根节点root,则:

进行操作1时,dt[x] ++

进行操作2时,dt[x] --, root ++;

进行操作3时,ut[x] ++;

进行操作4时,ut[x] --, root ++; 

最后向上或向下更新所有节点,然后求答案即可。  

代码如下:

#include<iostream>
using namespace std;
const int N = 1e7+5;
int dt[N], ut[N]; //向下更新的差分数组,和向上更新的差分数组 
int fg[N]; //用来记录每个权值的节点个数 
void dfs(int now, int n)
{
	if(now>n) return ;
	
	dt[now] += dt[now/2]; //向下更新 
	dfs(now*2, n);
	dfs(now*2+1, n);
	ut[now] += ut[now*2] + ut[now*2+1]; //向上更新 
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int n, m;
	cin >> n >> m;
	int root = 0; //根节点 
	for(int i=0; i<m; i++)
	{
		int op, x;
		cin >> op >> x;
		if(op==1) dt[x] ++;
		else if(op==2) dt[x] --, root ++;
		else if(op==3) ut[x] ++;
		else ut[x] --, root ++;
	}
	dfs(1, n);
	for(int i=1; i<=n; i++) fg[root+ut[i]+dt[i]] ++; //该节点的权值,即是两种更新方式最终的权值加上根节点记录的值 
	for(int i=0; i<=m; i++) cout << fg[i] << " ";
	cout << endl;
	return 0;
}

 

F.子串的子序列(dp)

链接:https://ac.nowcoder.com/acm/contest/47266/F
来源:牛客网
 

题目描述

牛牛获得了一个仅由小写字母构成的字符串 s 。

他想知道这个字符串中有多少个子区间满足:区间中含有子序列 "ac" ,且区间中包含的 "ac" 子序列为偶数个。

请你输出满足上述条件的子区间的个数。

输入描述:

一行一个字符串代表 s。
保证:
字符串 s 的长度不超过 5×1e5 
s 仅由小写字母组成

输出描述:

一行一个整数代表答案。

示例1

输入

acaacb

输出

6

说明

共有六个区间中包含偶数个 "ac" 子序列:
    1、区间 [1,5] 包含 4 个 "ac" 子序列
    2、区间 [1,6] 包含 4 个 "ac" 子序列
    3、区间 [2,5] 包含 2 个 "ac" 子序列
    4、区间 [2,6] 包含 2 个 "ac" 子序列
    5、区间 [3,5] 包含 2 个 "ac" 子序列
    6、区间 [3,6] 包含 2 个 "ac" 子序列

该题求子序列的个数,所以显然用dp,对于求区间内子序列ac是偶数个数,可以开个二维dp[i][j]数组,表示以i结尾的,ac个数的奇偶性为j的区间个数。

但由于,每次更新dp数组时,如果新的一个元素是'c'时,那么更新后新增的ac的个数还与 1~i-1 中a的个数有关,也就是更新后的ac的个数和奇偶性与其前面的a的个数和奇偶性有关,所以dp数组还需要开一个维度用来记录a的奇偶性。

所以可以开个数组dp[i][j][k], 表示以i位置结尾,a的个数奇偶性为j的,ac的个数奇偶性为k的 区间个数。(用0表示偶数,1表示奇数)

然后状态转移方程,首先

当上一个状态为dp[i-1][0][0]时,如果当前新增的一个元素为'a',那么a的数量加一,偶数变奇数,ac的数量不变,所以当前状态为dp[i][1][0] ;如果是'c',那么a的数量不变,a个数的奇偶性不变,ac的数量等于当前的偶数数量加上目前a的偶数数量,所以还是偶数,所以当前状态为dp[i][0][0]

当上一状态为dp[i-1][1][0]时,如果新增了一个'a',那么a的奇数个数变偶数,ac个数的奇偶性不变,所以dp[i][0][0],如果新增'c',a个数的奇偶性不变,ac的个数为之前的偶数加上a的个数奇数,变奇数,所以当前状态为dp[i][1][1]; 

当上一状态为dp[i-1][1][1],如果新增一个'a',那么a个数从奇变偶,ac个数奇偶性不变,所以当前状态为dp[i][0][1],如果新增'c',a个数奇偶性不变,ac个数奇偶性从奇变偶,当前状态为dp[i][1][0];

当上一状态为dp[i-1][0][1],如果新增一个'a',那么a的个数变为奇数,ac个数奇偶性不变,所以当前状态为dp[i][1][1],如果新增'c',a个数奇偶性不变,ac个数为奇数加a的偶数,还是奇数,奇偶性不变,所以当前状态为dp[i][0][1];

如果新增的元素不为'a'和'c',那么直接继承上一状态即可。 

同时由于每次新增一个元素时,存在一个区间[i, i],需要把该区间加上去,所以当新增元素为'a'时,dp[i][1][0] ++, 反之dp[i][0][0] ++; 

因此状态转移方程为:  

	if(s[i]=='c')
	{
		dp[i][0][0] ++;
		dp[i][0][0] += dp[i-1][0][0];
		dp[i][1][0] += dp[i-1][1][1];
		dp[i][1][1] += dp[i-1][1][0];
		dp[i][0][1] += dp[i-1][0][1];
	}
	else if(s[i]=='a')
	{
		dp[i][1][0] ++;
		dp[i][0][0] += dp[i-1][1][0];
		dp[i][1][0] += dp[i-1][0][0];
		dp[i][1][1] += dp[i-1][0][1];
		dp[i][0][1] += dp[i-1][1][1];
	}
	else
	{
		dp[i][0][0] ++;
		for(int j=0; j<2; j++) for(int k=0; k<2; k++) dp[i][j][k] += dp[i-1][j][k];
	}

最后,由于0也属于偶数,所以最终结果还需要减去区间中ac为0的情况的个数,具体实现代码如下:

#include<iostream>
#include<string>
#include<cstring>
using namespace std;
typedef long long LL;
const int N = 5e5+5;
LL dp[N][2][2], nea[N], nec[N], n; //nea[i], nec[i]分别表示当前位置为i时,下一个'a'所在位置和下一个'c'所在位置。 
char s[N];
LL NoAC() //求不存在ac的区间个数 
{
	nea[n+1] = nec[n+1] = n+1;
	for(int i=n; i>=1; i--)
	{
		nea[i] = nea[i+1];
		nec[i] = nec[i+1];
		if(s[i]=='c') nec[i] = i; //记录下一个'c'所在位置 
		if(s[i]=='a') nea[i] = i; //记录下一个'a'所在位置
	}
	LL num = 0;
	//nea[i]表示当前位置i的下一个'a'的位置, 则nec[nea[i]]表示下一个'a'位置的下一个'c'位置
	//如果区间要不包含ac的话,那么就绝对不能同时包含这两个位置。所以区间[i, nec[nea[i]]]的所有子区间必然不包含ac,所以求出该区间的子区间数即可 
	//每次求完后,得跳到nea[i]+1的位置。 
	for(LL i=1; i<=n; )
	{
		int ne = nea[i];
		for(LL len=1; len<=ne-i+1; len++)
			num += (nec[ne]-i)-len+1; //求子区间数
        i = ne + 1; //跳到下一个'a'的前面一个位置。 
	}
	
	//这是大佬的代码,虽然好像看懂了,但是说不清ort.... 
//	for(LL i=1; i<=n; i++)
//		num += nec[nea[i]] - i;
	
	return num;
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> s+1;
	n = strlen(s+1);
	LL res = 0;
	for(int i=1; i<=n; i++)
	{
		if(s[i]=='c')
		{
			dp[i][0][0] ++;
			dp[i][0][0] += dp[i-1][0][0];
			dp[i][1][0] += dp[i-1][1][1];
			dp[i][1][1] += dp[i-1][1][0];
			dp[i][0][1] += dp[i-1][0][1];
		}
		else if(s[i]=='a')
		{
			dp[i][1][0] ++;
			dp[i][0][0] += dp[i-1][1][0];
			dp[i][1][0] += dp[i-1][0][0];
			dp[i][1][1] += dp[i-1][0][1];
			dp[i][0][1] += dp[i-1][1][1];
		}
		else
		{
			dp[i][0][0] ++;
			for(int j=0; j<2; j++) for(int k=0; k<2; k++) dp[i][j][k] += dp[i-1][j][k];
		}
		res += dp[i][1][0] + dp[i][0][0]; 
	}
	cout << res - NoAC() << endl;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值