[2019 牛客CSP-S提高组赛前集训营1]仓鼠的石子游戏 + 乃爱与城市拥挤程度 + 小w的魔术扑克

文章目录

  • 前言
  • T1.仓鼠的石子游戏
    • 题目描述
    • 题解
    • 参考代码
  • T2.乃爱与城市拥挤程度
    • 题目描述
    • 题解
    • 参考代码
  • T3.小w的魔术扑克
    • 题目描述
    • 题解
    • 参考代码

前言

不知道为什么,比赛的时候数据好像很水,我隔壁大佬T2暴力过了八十??!我码正解思路line2算错了?!!
这告诉我们什么?!!考试的时候,一定要积极地打暴力,码完“正解”也写个if判一判

友情链接:牛客CSP-S提高组赛前集训营1 ☜戳介里

T1.仓鼠的石子游戏

题目描述

时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld

题目描述
仓鼠和兔子被禁止玩电脑,无聊的他们跑到一块空地上,空地上有许多小石子。兔子捡了很多石子,然后将石子摆成n个圈,每个圈由a[i]个石子组成。然后兔子有两根彩色笔,一支红色一支蓝色。兔子和仓鼠轮流选择一个没有上色的石子涂上颜色,兔子每次可以选择一个还未染色的石子将其染成红色,而仓鼠每次可以选择一个还未染色的石子将其染成蓝色,并且仓鼠和兔子约定,轮流染色的过程中不能出现相邻石子同色,谁不能操作他就输了。假设他们两个都使用了最优策略来玩这个游戏,并且兔子先手,最终谁会赢得游戏?

输入描述
第一行输入一个正整数T,表示有T组测试案例。
每组测试案例的第一行输入一个n,表示有n圈石子。 第二行输入n个正整数a[i],表示每个圈的石子数量。

输出描述
对于每组测试案例,如果兔子赢了,输出"rabbit"(不含引号)如果仓鼠赢了,输出"hamster"(不含引号)。
数据范围及提示
本题共有10组测试点数据。
对于前30%的数据,满足n = 1, 1 ≤ a[i] ≤ 7, 1 ≤ T ≤ 10。
对于前60%的数据,满足1 ≤ n ≤ 103, 1 ≤ a[i] ≤ 7, 1 ≤ T ≤ 102
对于前100%的数据,满足1 ≤ n ≤ 103,1 ≤ a[i] ≤ 109, 1 ≤ T ≤ 102
对于测试点6,在满足前60%的数据条件下,额外满足a[i] = 1。

样例输入输出
Sample Input
4
1
3
1
1
2
1 3
3
999 1000 1000000000
Sample Output
hamster
rabbit
rabbit
hamster
样例解释
对于第一组案例:只有1圈石子,并且石圈的大小为3。
兔子先手,随便找了一个石子染成红色,接下来仓鼠后手找一个未染色的石子染成蓝色,此时结果如下图所示。
[2019 牛客CSP-S提高组赛前集训营1]仓鼠的石子游戏 + 乃爱与城市拥挤程度 + 小w的魔术扑克_第1张图片
如果兔子将最后一个石子染成红色,这将导致相邻石子同色,根据规则,他输掉了比赛,所以仓鼠获得了最终的胜利。
对于第二组案例:只有1圈石子,并且石圈的大小为1。
兔子先手,将唯一的一个石子染成了红色,接下来由于没有未着色的石子,所以仓鼠由于无法操作而输掉了比赛,兔子取得了最终的胜利。
对于第三组案例:有两个石圈,大小分别为1,3,兔子首先将大小为1的石圈中唯一一个石子染成了红色,接下来仓鼠由于类似第一组案例中的原因输掉比赛,兔子取得了最终的胜利。

题解

(看巨佬们的博客,有说是SG函数的,然而本蒟蒻并不了解是怎么回事。。。)
这题一看就是一道博弈论,我也不知道该怎么说,在草稿纸上涂涂画画,自己推一下,就可以发现,除了石子数为一,其他情况都是先手必败


石子数为一先手必胜很好理解,那么我们接下来继续思考石子数不为一的情况:
想要让染色无法继续,有两种情况:
①.所有石子均被着色:
 a.石子数为偶数,那么最后一个着色的人会是后手,因此,先手必败;
 b.石子数为奇数,那么很容想到,不会出现全部着色的情况(颜色比冰交替出现),因此,先手必败。
②.再染任一一颗石子都会出现连续相同的颜色:
 那么所有此刻尚未着色的石子两边皆是颜色不同的两颗石子,由此易得,染色石子的出现必定是偶数
 个,那么最后一个着色的人也是后手,因此,先手还是必败
综上,先手好难啊 只有石子数为一的情况先手会赢
最后再判一下奇偶就好了

参考代码

#include 
#include 
using namespace std;

int T, n, ans;

int main () {
	scanf ("%d", &T);
	while (T --) {
		scanf ("%d", &n);
		for (int i = 1, x; i <= n; ++ i) {
			scanf ("%d", &x);
			if (x == 1)
				++ ans;
		}
		if (ans & 1)
			printf ("rabbit\n");
		else
			printf ("hamster\n");
	}
	return 0;
} 

T2.乃爱与城市拥挤程度

题目描述

时间限制:C/C++ 2秒,其他语言4秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld

题目描述
乃爱天下第一可爱!
乃爱居住的国家有n座城市,这些城市与城市之间有n-1条公路相连接,并且保证这些城市两两之间直接或者间接相连。
我们定义两座城市之间的距离为这两座城市之间唯一简单路径上公路的总条数。
当乃爱位于第x座城市时,距离城市x距离不大于k的城市中的人都会认为乃爱天下第一可爱!
认为乃爱天下第一可爱的人们决定到乃爱所在的城市去拜访可爱的乃爱。我们定义这些城市的拥挤程度为:
距离城市x距离不大于k的城市中的人到达城市x时经过该城市的次数。例如:
[2019 牛客CSP-S提高组赛前集训营1]仓鼠的石子游戏 + 乃爱与城市拥挤程度 + 小w的魔术扑克_第2张图片
假设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 109+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后的结果。

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

T2大样例下载连接:
https://pan.baidu.com/s/1AbBuEC8SmWlRMvtj6nLRkw
或者复制以下地址新建下载
https://uploadfiles.nowcoder.com/files/20191029/8030387_1572353828028_T2%20%E5%A4%A7%E6%A0%B7%E4%BE%8B.zip

样例输入输出
Sample Input
7 2
1 2
2 3
2 4
2 5
5 6
5 7
Sample Output
5 7 5 5 7 4 4
20 21 20 20 28 12 12

题解

首先锁定dp,然后思考状态转移方程式。。。
对于节点u而言,它的拥挤度来自它的祖先们,以及它的儿砸们;而祖先与儿子的拥挤度有事互不干扰,互不影响的,那么,我们可以定义一个 d p [ u ] [ i ] dp[u][i] dp[u][i]表示节点u影响向下i层一共可以提供的贡献。


首先考虑它的儿子们对于节点u的贡献。很容易发现,u节点往下的子树,不管向上转移多少层,它们子树内部的贡献总是不变的,因此,我们可以直接考虑节点u因此而造成的拥挤度而产生的贡献,即是它的儿子的个数,因此,我们得出状态转移方程式:
s o n [ u ] [ j ] = ∑ i = 0 s i z e ( ) − 1 s o n [ G [ u ] [ i ] ] [ j − 1 ] son[u][j] = \sum_{i = 0}^{size() - 1}son[G[u][i]][j - 1] son[u][j]=i=0size()1son[G[u][i]][j1]
d p [ u ] [ j ] = ∏ i = 0 s i z e ( ) − 1 d p [ G [ u ] [ i ] ] [ j − 1 ] ∗ s o n [ u ] [ j ] dp[u][j] = \prod^{size() - 1}_{i = 0}dp[G[u][i]][j - 1] * son[u][j] dp[u][j]=i=0size()1dp[G[u][i]][j1]son[u][j]其中, G [ u ] [ i ] G[u][i] G[u][i]表示u的第i个儿子, s o n [ u ] [ i ] son[u][i] son[u][i]表示节点u向下第i层的所有儿子节点个数
需要注意的是,每一处状态转移都需要考虑层数的差距,上面这个式子就是,到u需要经过j条路径,那么到它的儿子就只需经过j-1条路径,再有它的儿子经过一条路径到达u,长度共计为j。


接下来,我们继续考虑从它的祖先转移的情况。
对于节点v的祖先u而言,u所扩展出去的节点一定会经过u,从u到v的距离为dis,那么u能扩展出去的节点距离u最长为k - dis,其次对于祖先来说,节点u不仅可以继续向上扩展,还可以像u的其他儿子扩展,因此,递推需要先解决祖先再解决儿子,即,先递推,再递归。

仔细想一下,会发现如果直接把祖父节点和儿子节点的贡献乘起来,会有重复计算的部分,那么我们需要进行的下一步工作就是去重。

  • 首先考虑节点v本身,它的贡献会多出祖先节点可以走到的非v节点子树的节点,那么,我们定义这些多出来的节点个数为t t = s o n [ u ] [ k − 1 ] − s o n [ v ] [ k − 2 ] t = son[u][k - 1] - son[v][k - 2] t=son[u][k1]son[v][k2]
  • 然后,我们继续考虑它祖先节点的贡献,由于我们是先递推,后递归,因此,它的父亲节点是被重新定义过的(即包含子树以及祖先的贡献),就包含了所有祖先节点的贡献,就简化了我们的计算过程,只需考虑父亲节点的多出来的贡献即可。接下来,画一画图就可以知道,祖先节点多出来的贡献可以表示为: v a l = d p [ u ] [ k − 1 ] / d p [ v ] [ k − 2 ] / s o n [ v ] [ k ] ∗ t val = dp[u][k - 1] / dp[v][k - 2] / son[v][k] * t val=dp[u][k1]/dp[v][k2]/son[v][k]t
  • 下面解释这个式子,很容易理解, d p [ u ] [ k − 1 ] dp[u][k - 1] dp[u][k1]表示与节点u距离为k - 1的所有节点的贡献(包括子树以及祖先), d p [ v ] [ k − 2 ] dp[v][k - 2] dp[v][k2]表示与节点u距离为k - 1的儿子们的贡献,除掉之后,即是v节点向上的错误贡献;这半个式子只错在节点u的贡献,那么节点u的贡献错在哪?易得,节点u多计算了距离为k - 1但位于v的子树中的点,那么再除掉 s o n [ v ] [ k ] son[v][k] son[v][k]就可去掉这部分错误贡献,接着乘上节点u的正确贡献 t t t就得到了正确的贡献。
  • 没看懂的不要怕,代码中还有注释 (虽然我觉着并没有什么luan用)

需要注意一下,在代码实现中,除法的部分需要用逆元来计算,这是常识。。。

参考代码

#include 
#include 
#include 
using namespace std;
 
const int N = 1e5;
const int mod = 1e9 + 7;
int n, k, son[N + 5][15];
long long dp[N + 5][15];
vector < int > G[N + 5];
 
inline long long qkpow (long long x, int power) {
    long long res = 1ll;
    while (power) {
        if (power & 1)
            res = (res * x) % mod;
        x = (x * x) % mod;
        power >>= 1;
    }
    return res;
}
inline long long inv (const long long x) { return qkpow (x, mod - 2); }
 //费马小定理求逆元
 
void dfs1 (const int u, const 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 (const int u, const 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) {
            int Invv = inv (son[v][j + 1]), Invu = inv (son[u][j]), Invt = j ? inv (dp[v][j - 1]) : 1;
            int t = son[u][j] - ( j ? son[v][j - 1] : 0 );	//向上走会多出来的节点
            dp[v][j + 1] = dp[v][j + 1] * Invv % mod;	//v的子树除掉v本身的贡献
            son[v][j + 1] += t;	//v节点可以走到的所有节点
            dp[v][j + 1] = dp[v][j + 1] * son[v][j + 1] % mod
                           * dp[u][j] % mod * Invu % mod * t % mod * Invt % mod;
            //dp[v][j + 1] * son[v][j + 1]:重新定义后,v的子树以及v节点所提供的贡献
            //dp[u][j] * Invu:考虑向上走的贡献,除掉了在dp[u][j]中已经算过且是错误的son[u][j]
            //t * Invt:多出来的节点(t)才是u节点的真正贡献,由于其它节点的贡献在dp[u][j]中已算过,故除掉
        }
        dfs2 (v, u);
    }
}
 
int main () {
    scanf ("%d %d", &n, &k);
    for (int i = 2; 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 ("%d%c", son[i][k], i == n ? '\n' : ' ');
    for (int i = 1; i <= n; ++ i)
        printf ("%lld%c", dp[i][k], i == n ? '\n' : ' ');
    return 0;
}

T3.小w的魔术扑克

题目描述

时间限制:C/C++ 1秒,其他语言2秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld

题目描述
小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"表示不可以。(不含引号)
每个查询都是独立的,查询之间互不影响。

数据范围及提示
本题共有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%的测试点,保证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%的测试点条件下,额外保证所有卡牌正面上的数字等于其反面上的数字,但不同扑克牌上的数字不保证相同。
T3大样例下载:
https://pan.baidu.com/s/1pNRQ4PlN0GQi5AHb9QgKXA
或者复制以下地址新建下载:
https://uploadfiles.nowcoder.com/files/20191029/8030387_1572353843639_T3%20%E5%A4%A7%E6%A0%B7%E4%BE%8B.zip

样例输入输出
Sample Input 1
5 3
1 2
2 3
4 4
3
1 2
2 4
1 4
Sample Output 1
Yes
Yes
No
样例解释 1
对于顺子1~2,可以选择第一张卡牌作为’1’使用,选择第二张卡牌作为’2’使用。
对于顺子2~4,可以选择第一张卡牌作为’2’使用,选择第二张卡牌作为’3’使用,选择第三张卡牌作为’4’使用。
对于顺子1~4,由于牌的数目都不够,显然无法打出。

Sample Input 2
4 3
1 1
2 2
4 4
3
1 2
1 4
4 4
Sample Output 2
Yes
No
Yes
样例解释 2
该样例具有测试点4的特殊性质。

题解

首先你得想到图论模型 (其实我也不知道这是怎么想到的)
想到图论,你就对了一大半了。
首先我们把每一张牌正反两面之间建一条边连起来,构建出图论模型。
接着,一张n个点m条边的图就建成了。
然后,我们继续考虑这张图的结构:
首先,它必定会是一个或几个连通块,

  • 对于每个连通块,若连通块中有n个点,大于n条边,那么这个连通块中的任意点都可以随便取
  • 若只有n个点n - 1条边,那么这n个点中只有n - 1个点可以取到

因此,只有结构为树的连通块对于牌的取值是有约束的。
那么,我们可以用各种手段维护连通块以及结构(像什么并查集,树状数组),然后再判断是否可以覆盖整个区间的数字就可以了
下面上代码☟

参考代码

#include  
#include 
using namespace std;

const int N = 1e5;
int n, k, m, fa[N + 5], maxx[N + 5], minn[N + 5], dp[N + 5];
//maxx[]/minn[]:每个连通块的最大/最小值
//dp[]:右端点取i可以取到的最小左端点 - 1
bool ok[N + 5];
void buildSet () {
	for (int i = 1; i <= n; ++ i)
		fa[i] = maxx[i] = minn[i] = i;
}
int findSet (const int u) {
	if (u != fa[u]) fa[u] = findSet (fa[u]);
	return fa[u];
} 
inline bool unionSet (int &u, int &v) {
	u = findSet (u); v = findSet (v);
	if (u == v) return true;
	fa[v] = u; return false;
}

void init () {
	for (int i = 1; i <= k; ++ i) {
		int u, v;
		scanf ("%d %d", &u, &v);
		if (unionSet (u, v))	//原本就是连通的,再加一条边,必定不是树结构
			ok[u] = true;
		else {					//两个点连接起来有可能树结构
			ok[u] |= ok[v];		//考虑之前有一个连通块本来就不是树结构
			//注意这个地方的u和v与unionSet函数对应
			maxx[u] = max (maxx[u], maxx[v]);
			minn[u] = min (minn[u], minn[v]);
		}
	}
}

void sovle () {
	//考虑树结构对于取牌的限制
	for (int i = 1, father_; i <= n; ++ i)
		if (! ok[father_  = findSet(i)])
			dp[maxx[father_]] = minn[father_];
	//利用顺子的约束递推
	for (int i = 1; i <= n; ++ i)
		dp[i] = max (dp[i], dp[i - 1]);
}

int main () {
	scanf ("%d %d", &n, &k);
	buildSet ();
	init ();
	sovle ();
	scanf ("%d", &m);
	for (int i = 1; i <= m; ++ i) {
		int l, r; scanf ("%d %d", &l, &r);
		//注意不能取等
		//具体原因可以自己画图考虑右端点为r时的转移情况(本身和上一个值),我口胡不清。。。
		if (dp[r] < l) printf ("Yes\n");
		else printf ("No\n");
	}
	return 0;
}

你可能感兴趣的:([2019 牛客CSP-S提高组赛前集训营1]仓鼠的石子游戏 + 乃爱与城市拥挤程度 + 小w的魔术扑克)