牛客网CSP-S提高组赛前集训营1题解(仓鼠的石子游戏 [博弈论] + 乃爱与城市的拥挤程度 [树上DP] + 小w的魔术扑克[dfs + 离线])

T1:仓鼠的石子游戏

题目

仓鼠和兔子被禁止玩电脑,无聊的他们跑到一块空地上,空地上有许多小石子。兔子捡了很多石子,然后将石子摆成n个圈,每个圈由a[i]个石子组成。然后兔子有两根彩色笔,一支红色一支蓝色。兔子和仓鼠轮流选择一个没有上色的石子涂上颜色,兔子每次可以选择一个还未染色的石子将其染成红色,而仓鼠每次可以选择一个还未染色的石子将其染成蓝色,并且仓鼠和兔子约定,轮流染色的过程中不能出现相邻石子同色,谁不能操作他就输了。假设他们两个都使用了最优策略来玩这个游戏,并且兔子先手,最终谁会赢得游戏?
输入描述:
第一行输入一个正整数T,表示有T组测试案例。
每组测试案例的第一行输入一个n,表示有n圈石子。 第二行输入n个正整数a[i],表示每个圈的石子数量。
输出描述:
对于每组测试案例,如果兔子赢了,输出”rabbit“(不含引号)如果仓鼠赢了,输出"hamster"(不含引号)。
示例1
输入
4
1
3
1
1
2
1 3
3
999 1000 1000000000
输出
hamster
rabbit
rabbit
hamster
说明
对于第一组案例:只有1圈石子,并且石圈的大小为3
兔子先手,随便找了一个石子染成红色,接下来仓鼠后手找一个未染色的石子染成蓝色,此时结果如下图所示
在这里插入图片描述
如果兔子将最后一个石子染成红色,这将导致相邻石子同色,根据规则,他输掉了比赛,所以仓鼠获得了最终的胜利。
对于第二组案例:只有1圈石子,并且石圈的大小为1。
兔子先手,将唯一的一个石子染成了红色,接下来由于没有未着色的石子,所以仓鼠由于无法操作而输掉了比赛,兔子取得了最终的胜利。
对于第三组案例:有两个石圈,大小分别为1,3,兔子首先将大小为1的石圈中唯一一个石子染成了红色,接下来仓鼠由于类似第一组案例中的原因输掉比赛,兔子取得了最终的胜利。

备注:
本题共有10组测试点数据。
对于前30%的数据,满足n=1,1⩽a[i]⩽7,1⩽T⩽10
对于前60%的数据,满足1⩽n⩽103,1⩽a[i]⩽7,1⩽T⩽102
对于前00%的数据,满足1⩽n⩽103,1⩽a[i]⩽109,1⩽T⩽102
对于测试点6,在满足前60%的数据条件下,额外满足a[i]=1。

题解

其实这就是一道博弈论,我们画一画奇环,偶环,推一推就找到了
ⅰ:奇环
在这里插入图片描述
再加上一点点理论理想推导:
因为这是一个环,先手选了点过后,后手就跟着它后面瞬移一个选点,到最后肯定都会剩下一个点
而这个点左右一定不是相同颜色(n-1为偶数,如果相同,一定会有两个颜色点在某处相同),
所以这个点就死了,先后手都无法填充


ⅱ:偶环
在这里插入图片描述
按照我们之前填充的方法,不难发现最后这个偶环肯定是可以被填充完的,先后手都不会输掉


综上不管遇到奇环或者偶环,先后手都不会死亡,并且处理的当前环以后是无法继续填充任何一种颜色的
所以不管哪只动物变成填充环的第一个颜色,都是一样的效果
那最后不永远都是后手没有地方可以选择过后输掉比赛吗?
难道一直都是先手赢吗?样例告诉我们显然不是。。。
观察样例在这里插入图片描述我们发现,真正改变输赢的就是自环1
因为单独成环的点让无路可走的后手有了一个地方,这样就把先手逼到了死角
但是如果又出现了一个单独成环的点,先手又重新掌握了胜局

总结一下,这道题就是判一个单独成环的点的个数的奇偶性,水吧~~~~
在这里插入图片描述


代码实现

#include <cstdio>
#define MAXN 1005
int T, n, tot;
int a[MAXN];
int main() {
    scanf ( "%d", &T );
    while ( T -- ) {
        scanf ( "%d", &n );
        tot = 0;
        for ( int i = 1;i <= n;i ++ ) {
            scanf ( "%d", &a[i] );
            if ( a[i] == 1 )
                tot ++;
        }
        if ( tot % 2 )
            printf ( "rabbit\n" );
        else
            printf ( "hamster\n");
    }
    return 0;
}

接下来就是这篇博客的重点所在了,写这篇博客的意义就在于这一道题

T2:乃爱与城市拥挤程度

题目

乃爱天下第一可爱!
乃爱居住的国家有n座城市,这些城市与城市之间有n-1条公路相连接,并且保证这些城市两两之间直接或者间接相连。

我们定义两座城市之间的距离为这两座城市之间唯一简单路径上公路的总条数。
当乃爱位于第x座城市时,距离城市x距离不大于k的城市中的人都会认为乃爱天下第一可爱!
认为乃爱天下第一可爱的人们决定到乃爱所在的城市去拜访可爱的乃爱。
我们定义这些城市的拥挤程度为:
距离城市x距离不大于k的城市中的人到达城市x时经过该城市的次数。例如:
在这里插入图片描述
假设k=2,乃爱所在的城市是1号城市,树结构如上图所示时,受到影响的城市为1,2,3,4,5,因为五个城市距离1号城市的距离分别为:0,1,2,2,2,所以这五个城市都会认为乃爱天下第一。
1号城市到1号城市经过了1号城市。
2号城市到1号城市经过了1号、2号城市。
3号城市到1号城市经过了1号、2号、3号城市。
4号城市到1号城市经过了1号、2号、4号城市。
5号城市到1号城市经过了1号、2号、5号城市。
所以1号城市的拥挤程度是5,2号城市的拥挤程度是4,3号、4号、5号城市的拥挤程度都是1。

现在小w想要问你当乃爱依次位于第1、2、3、4、5…n座城市时,有多少座城市中的人会认为乃爱天下第一,以及受到影响城市的拥挤程度的乘积,由于这个数字会很大,所以要求你输出认为乃爱天下第一的城市拥挤程度乘积mod 10^9+7后的结果。

输入描述:
第一行是两个正整数n,k表示城市数目,
以及距离乃爱所在城市距离不大于k的城市中的人认为乃爱天下第一!
接下来n-1行,每行两个正整数u,v,表示树上一条连接两个节点的边。
输出描述:
输出两行
第一行n个整数,表示当乃爱依次位于第1、2、3、4、5…n座城市时,
有多少座城市中的人会认为乃爱天下第一
第二行n个整数,表示当乃爱依次位于第1、2、3、4、5…n座城市时,
受影响的城市拥挤程度乘积mod 10^9+7后的结果。

示例1
输入
7 2
1 2
2 3
2 4
2 5
5 6
5 7
输出
5 7 5 5 7 4 4
20 21 20 20 28 12 12

备注:
本题共有10组测试点数据。
对于前10%的测试点满足1⩽n⩽10,1⩽k⩽10,树结构随机生成
对于前30%的测试点满足1⩽n⩽103,1⩽k⩽10,树结构随机生成
对于前70%的测试点满足1⩽n⩽105,1⩽k⩽10,树结构随机生成
对于前1100%的测试点满足1⩽n⩽105,1⩽k⩽10,树结构为手动构造
对于测试点4,在满足其前70%的测试点条件下,额外满足k=1
对于测试点5,在满足其前70%的测试点条件下,额外满足k=2
对于测试点10,在满足其前100%的测试点条件下,额外满足树结构退化成一条链

题解

在这里插入图片描述
我们设 s o n [ i ] [ j ] son[i][j] son[i][j]:表示以i作为根节点时,与i的距离小于等于j的所有的节点数
同时设 d p [ i ] [ j ] dp[i][j] dp[i][j]:表示以i作为根节点时,与i的距离小于等于j的所有节点数的拥挤程度的乘积
那么答案自然而然就是 s o n [ i ] [ k ] , d p [ i ] [ k ] son[i][k],dp[i][k] son[i][k],dp[i][k]

前提:要明白单个点x对于答案的贡献就是乘上它的拥挤程度也就是它的儿子节点数son


首先我们就以1为根节点,先跑一遍树上DP,得到最初的所有 s o n [ i ] [ j ] , d p [ i ] [ j ] son[i][j],dp[i][j] son[i][j],dp[i][j]
接着就再跑一次DP,得到以x为根节点的每一种情况的对应答案
那么我们怎么进行转移呢???
在这里插入图片描述
上图讲解:↓图很重要哦!!!建议大家多画一画跟着下面一步步推,确实有点难以理解
在这里插入图片描述
假设现在我们正在u节点,与u的距离为j,要更新v节点,与v的距离为j+1的答案时


(1):
可以知道要 求与v的距离为j时,v所选的所有子树会全部被包含到 求与v的父亲u的距离为j+1时的答案中
并且这些子树的贡献是不变的,


(2):
当我们选v为根节点的时候,建立在(1)的基础上,就会发现我们统计了一些错误的节点,


①:u的答案贡献明显错误,因为包含在v的子树中的节点被u错误计算了,要除掉
看图↑:
u对v的贡献就要排除掉被v包含的v,v1,v2三个点的贡献,
所以u对v的贡献就是红圈减去红圈与黄圈的并集

如果v1,v2与u的距离为j的话,v1,v2与v的距离就只有j-1
所以此时的状态转移方程式就是:
d p [ v ] [ j + 1 ] = d p [ v ] [ j + 1 ] ∗ i n v ( s o n [ u ] [ j ] ) dp[v][j+1]=dp[v][j+1]*inv(son[u][j]) dp[v][j+1]=dp[v][j+1]inv(son[u][j])% m o d ∗ ( s o n [ u ] [ j ] − ( j ? s o n [ v ] [ j − 1 ] : 0 ) ) mod*(son[u][j] - ( j ? son[v][j - 1] : 0 )) mod(son[u][j](j?son[v][j1]:0))

inv(x)表示x在模mod意义下的逆元
乘以 i n v ( s o n [ u ] [ j ] ) inv(son[u][j]) inv(son[u][j])就意味者先把错误的u的贡献除掉
乘以 s o n [ u ] [ j ] − ( j ? s o n [ v ] [ j − 1 ] : 0 ) son[u][j] - ( j ? son[v][j - 1] : 0 ) son[u][j](j?son[v][j1]:0)意味着乘上真正的u的贡献

注意:如果j为0意味着只包含u自己,他就不会发生对v的转移
在这里插入图片描述


②v自己的贡献也明显错误,因为v可以通过u再扩散j层,它本身的拥挤程度就该加上u扩散的第j层的个数
当然这中间还是会包含到①所提到的在v的子树范围内的某些节点,
看图↑:
如果v1,v2与b1,b2,b3,b4是u为根时同一层的节点,b1,b2,b3,b4代表着u的其他子树里面的节点
b1,b2…与u的距离为j-1的话,b1,b2…与v的距离就是j,但是v1,v2与v的距离是j-2
v的拥挤程度就要多上b1,b2,b3,b4这一层的点,但是这一层里面包含了v的子节点v1,v2,必须减掉

所以此时的状态转移方程式是:
s o n [ v ] [ j + 1 ] = s o n [ v ] [ j + 1 ] + s o n [ u ] [ j ] − ( j ? s o n [ v ] [ j − 1 ] : 0 ) son[v][j+1]=son[v][j+1]+son[u][j] - ( j ? son[v][j - 1] : 0 ) son[v][j+1]=son[v][j+1]+son[u][j](j?son[v][j1]:0)
d p [ v ] [ j + 1 ] = d p [ v ] [ j + 1 ] ∗ i n v ( s o n [ v ] [ j + 1 ] ) dp[v][j+1]=dp[v][j+1]*inv(son[v][j + 1]) dp[v][j+1]=dp[v][j+1]inv(son[v][j+1])% m o d ∗ s o n [ v ] [ j + 1 ] mod*son[v][j+1] modson[v][j+1]% m o d mod mod
乘以 i n v ( s o n [ v ] [ j + 1 ] ) inv(son[v][j + 1]) inv(son[v][j+1])意味着先把错误的v的贡献除掉
s o n [ v ] [ j + 1 ] son[v][j+1] son[v][j+1]的转移就是v的真正包含的节点数
最后再乘以 s o n [ v ] [ j + 1 ] son[v][j+1] son[v][j+1]就是算上了正确的v的贡献


③:算了u,v各自的真正贡献,还要乘上原本的 d p [ u ] [ j ] dp[u][j] dp[u][j]这时候这里面就是正确的贡献乘积
但是我们发现,答案会错误,因为我们把 d p [ v ] [ j − 1 ] dp[v][j-1] dp[v][j1]的总体贡献多算了两次
d p [ v ] [ j − 1 ] dp[v][j-1] dp[v][j1]的贡献既在 d p [ v ] [ j + 1 ] dp[v][j+1] dp[v][j+1]原本的答案中,又被包含在了 d p [ u ] [ j ] dp[u][j] dp[u][j]中,
所以我们得把它剔除掉一次,也就是乘上它的逆元


最后真正的DP转移方程式就是综合以上所有情况
在这里插入图片描述

代码实现

如果实在看不懂dfs2的,可以评论告诉我,我再解释得更详细一点

#include <cstdio>
#include <vector>
using namespace std;
#define mod 1000000007
#define LL long long
#define MAXN 100005
vector < int > G[MAXN]; 
int n, k;
LL dp[MAXN][15], son[MAXN][15];

LL qkpow ( LL x, LL y ) {
	LL ans = 1;
	while ( y ) {
		if ( y & 1 )
			ans = ans * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return ans;
}

void dfs1 ( int u, int fa ) {
	for ( int i = 0;i <= k;i ++ )
		dp[u][i] = son[u][i] = 1;
	for ( int i = 0;i < G[u].size();i ++ ) {
		int v = G[u][i];
		if ( v == fa ) 
			continue;
		dfs1 ( v, u );
		for ( int j = 1;j <= k;j ++ ) {
			son[u][j] += son[v][j - 1];
			dp[u][j] = dp[u][j] * dp[v][j - 1] % mod;
		}
	}
	for ( int i = 1;i <= k;i ++ )
		dp[u][i] = dp[u][i] * son[u][i] % mod;
}

void dfs2 ( int u, int fa ) {
	for ( int i = 0;i < G[u].size();i ++ ) {
		int v = G[u][i];
		if ( v == fa )
			continue;
		for ( int j = k - 1;j >= 0;j -- ) {
			LL inv_u = qkpow ( son[u][j], mod - 2 );
			LL inv_v = qkpow ( son[v][j + 1], mod - 2 );
			LL inv_same = j ? qkpow ( dp[v][j - 1], mod - 2 ) : 1;
			
			dp[v][j + 1] = dp[v][j + 1] * inv_v % mod;
			
			LL tson = son[u][j] - ( j ? son[v][j - 1] : 0 );
			son[v][j + 1] += tson;
			
			dp[v][j + 1] = dp[v][j + 1] * son[v][j + 1] % mod * dp[u][j] % mod * inv_u % mod * tson % mod * inv_same % mod;
		}
		dfs2 ( v, u );
	}
}

int main() {
	scanf ( "%d %d", &n, &k );
	for ( int i = 1;i < n;i ++ ) {
		int u, v;
		scanf ( "%d %d", &u, &v );
		G[u].push_back( v );
		G[v].push_back( u );
	}
	dfs1 ( 1, 0 );
	dfs2 ( 1, 0 );
	for ( int i = 1;i < n;i ++ )
		printf ( "%lld ", son[i][k] );
	printf ( "%lld\n", son[n][k] );
	for ( int i = 1;i <= n;i ++ ) 
		printf ( "%lld ", dp[i][k] );
	return 0;
}

T3:小w的魔术扑克

题目

小w喜欢打牌,某天小w与dogenya在一起玩扑克牌,这种扑克牌的面值都在1到n,原本扑克牌只有一面,而小w手中的扑克牌是双面的魔术扑克(正反两面均有数字,可以随时进行切换),小w这个人就准备用它来出老千作弊。小w想要打出一些顺子,我们定义打出一个l到r的顺子需要面值为从l到r的卡牌各一张。小w想问问你,他能否利用手中的魔术卡牌打出这些顺子呢?
输入描述:
首先输入一行2个正整数n,k,表示牌面为1~n,小w手中有k张魔术扑克牌。
然后输入k行,每行两个数字,表示卡牌的正面和反面的面值。
接下来输入一行一个正整数q,表示q组查询,然后每组占一行查询输入两个整数l,r。表示查询小w能否打出这么一个l到r的顺子。
输出描述:
对于输出"Yes"表示可以,"No"表示不可以。(不含引号)
每个查询都是独立的,查询之间互不影响。
示例1
输入
5 3
1 2
2 3
4 4
3
1 2
2 4
1 4
输出
Yes
Yes
No
说明
对于顺子1~2,可以选择第一张卡牌作为’1’使用,选择第二张卡牌作为’2’使用
对于顺子2~4,可以选择第一张卡牌作为’2’使用,选择第二张卡牌作为’3’使用,选择第三张卡牌作为’4’使用
对于顺子1~4,由于牌的数目都不够,显然无法打出

示例2
输入
4 3
1 1
2 2
4 4
3
1 2
1 4
4 4
输出
Yes
No
Yes
说明
该样例具有测试点4的特殊性质。

备注:
本题共有10组测试点数据。
对于前10%的测试点,保证1⩽n⩽10,1⩽k⩽10,1⩽q⩽10,1⩽l⩽r⩽n
对于前20%的测试点,保证1⩽n⩽11,1⩽k⩽10,1⩽q⩽100,1⩽l⩽r⩽n
对于前30%30%的测试点,保证1⩽n⩽50,1⩽k⩽50,1⩽q⩽500,1⩽l⩽r⩽n
对于前100%的测试点,保证1⩽n⩽105, 1⩽k⩽105,1⩽q⩽105,1⩽l⩽r⩽n
对于测试点4,在满足前100%的测试点条件下,额外保证所有卡牌正面上的数字等于其反面上的数字,但不同扑克牌上的数字不保证相同。

题解

真的要吐槽一波用心做题目,用脚造数据
1.你忽视牌的背面,不管怎样询问都只用牌的正面,据说有50pots
2.你把牌的两面一次性都用上,据说有70pots
我有必要花大量时间去搞一个正解???
在这里插入图片描述


接下来进入100pots部分:
一张牌只能变成正面或者反面,我们就把这张牌建一条边,把两个值连接起来
这样我们就把1~n的牌值处理成了三个部分:
①没有一张牌能变成i,自环;②部分牌值形成了一棵树;③部分牌值形成了一个环(连通块)
并且不同的连通块,树是彼此独立的,不然它们应该能形成一个更大的树或者连通块


可以知道,如果对于一个连通块,边数大于等于点数,
那么这个连通块里面的每一个值都可以一次性全部出现

对于一棵树,边数小于点数,
那么这棵树里面最多只能同时出现点数-1的值,不可能同时出现

因为我们建的边就相当于一张牌,边连接的两个节点就相当于正反面的值
一条边只能选择所连的一个节点进行呈现,树就意味着少了一条边,也就少了一个点拥有呈现的机会

接下来我用类比的思想来帮助大家理解:假设我们有10项作业

树:就意味着我们手上只有9个作业本,那么如果老师一次性要收全部作业,咱们就GG了
但如果老师作业只收≤9项,那我们就可以完成

连通块:就意味着我们手上有≥10个作业本,不管老师要收多少项作业,咱都能全部交上


所以我们就可以抛开连通块不管,只考虑树的情况
由于树不能完全呈现它的所有值,所以当有一个查询包含了一棵完整的树时,那棵树总是无法完成任务

那么我们就考虑怎么判断这种情况:对每一棵树进行限定
限定当L小于等于Min(树上的最小值),R必须严格小于Max(树上的最大值)
因为一旦L小于等于Min,R大于等于Max,意味着这棵树被完全包含,肯定是无解的

最后当L小于等于Min的时候,R的限制会有多个,因为树有可能有多棵,我们就需要取并集,最小的R限制
举个栗子:有一棵树Min1=5,Max1=10,那么这棵树的限制就是当L小于等于5时,R一定不能大于等于10
又有另一棵树Min2=3,Max2=7,那么这棵树的限制就是当L小于等于3时,R一定不能大于等于7

综合这两棵树后:当L小于等于3,R就不能超过6,当3<L≤5,R就不能超过9

注意:不要以为一棵树一定出现了[Min,Max]里的每一个值,不然上面的解释你会认为是错的


接着有一些本博主码的时候,稍微混了一会的地方
⑴一个值只会出现在一棵树或者一个环或者单独成环,不会交错,
因为一旦有这种情况,这个值会成为一个纽扣将两个树或环连接在一起

⑵对于自环的点,我们也可以把它变成一棵树,无非就是把r的限制赋值成自己就可以解决了


代码实现

#include <cstdio>
#include <vector>
#include <iostream>
using namespace std;
#define MAXN 100005
#define INF 0x7f7f7f7f
int n, k, q, Max, Min, edge, dot;
vector < int > G[MAXN];
int limit[MAXN];
bool vis[MAXN];

void dfs ( int u ) {
	if ( vis[u] )//虽然这个点被遍历过了,dot没有增加,,但是通往它的边还是增加了
		return;
	dot ++;
	vis[u] = 1;
	Max = max ( Max, u );
	Min = min ( Min, u );
	for ( int i = 0;i < G[u].size();i ++ ) {
		edge ++;
		dfs ( G[u][i] );
	}
}

int main() {
	scanf ( "%d %d", &n, &k );
	for ( int i = 1;i <= k;i ++ ) {
		int Front, Back;
		scanf ( "%d %d", &Front, &Back );
		G[Front].push_back ( Back );
		G[Back].push_back ( Front );
	}
	for ( int i = 1;i <= n;i ++ )
		limit[i] = INF;
	for ( int i = 1;i <= n;i ++ ) {
		if ( ! G[i].size() )
			limit[i] = i;
		else if ( ! vis[i] ) {
			Max = - INF;
			Min = INF;
			edge = 0;
			dot = 0;
			dfs ( i );
			edge >>= 1;//每一条边我们都遍历了两次
			if ( edge < dot )//说明这是一棵树
				limit[Min] = Max;
		}
	}
	for ( int i = n - 1;i >= 1;i -- )
		limit[i] = min ( limit[i + 1], limit[i] );
	scanf ( "%d", &q );
	for ( int i = 1;i <= q;i ++ ) {
		int l, r;
		scanf ( "%d %d", &l, &r );
		if ( limit[l] <= r )
			printf ( "No\n" );
		else
			printf ( "Yes\n" );
	}
	return 0;
}

在这里插入图片描述再见,我想先安抚一下幼小的自己。。。。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值