CSDN竞赛10期题解

总结

这次竞赛题目难度还是不小的,最后两道难度都是中等以上,虽然都是一些竞赛原题,对于竞赛选手而言,刷过的是可以秒掉的;对于非竞赛选手而言,第一次遇见做出来还是不容易的。所以这次竞赛这么多高分,要么是突然涌进一堆竞赛选手,要么就是存在些许猫腻了,csdn竞赛的一个很大问题就是命题不知道创新,都是可以搜到的题目,不能保证竞赛的公平性,相对于其他的周赛,比如leetcode、acwing,每次周赛题目都是新出的,计时也是以比赛开始就计时,而不是以进入时间开始计时,公平性就要有保障了。

csdn的竞赛如果没有合适的命题人,找到题目后,也可以改一下题目背景和输入变量名,至少保证不能被搜到。

抛开这些问题不说,对于非竞赛选手,这次题目的质量还是不错的。

题目列表

1.熊孩子拜访

题目描述

已知存在一个长度为n的整数序列A。 A中所有元素按照从小到达的顺序进行排序。 现在执行操作倒置一段序列。 请找到 A序列里的倒置子序列。

分析

看见这题的第一反应应该是排序,然后比较下两个序列不同的地方。追求效率的我考试时非得用线性的解法去算,写完来了个WA,立马改暴力做法AC了,还是暴力大法好啊。

代码

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> t;
std::vector<int> solution(int n, std::vector<int>& vec) {
	t = vec;
	sort(t.begin(),t.end());
	for(int i = 0; i < n; i++) {
		if(t[i] != vec[i]) return {t[i],vec[i]};
	}
	return {0,0};
}
int main() {
	int n;
	std::vector<int> vec;
	std::cin>>n;
	std::string line_0, token_0;
	getline(std::cin >> std::ws,line_0);
	std::stringstream tokens_0(line_0);
	while(std::getline(tokens_0, token_0, ' ')) {
		vec.push_back(std::stoi(token_0));
	}
	std::vector<int> result = solution(n,vec);
	for(auto it=result.begin(); it!=result.end(); ++it) {
		std::cout<<*it<<" ";
	}
	std::cout<<std::endl;
	return 0;
}

2.走楼梯

题目描述

现在有一截楼梯,根据你的腿长,你一次能走 1 级或 2 级楼梯,已知你要走 n 级楼梯才能走到你的目的楼层,请实现一个方法,计算你走到目的楼层的方案数。

分析

经典走台阶问题,需要注意的是数据范围是50,需要使用long long存储才能AC。

代码

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
typedef long long ll;
ll f[55];
ll solution(int n) {
	f[0] = f[1] = 1;
	for(int i = 2; i <= n; i++) {
		f[i] = f[i-1] + f[i-2];
	}
	return f[n];
}
int main() {
	int n;
	std::cin>>n;
	ll result = solution(n);
	std::cout<<result<<std::endl;
	return 0;
}

3. 括号上色

题目描述

小艺酱又得到了一堆括号。 括号是严格匹配的。 现在给括号进行上色。 上色有三个要求: 1、只有三种上色方案,不上 色,上红色,上蓝色。 2、每对括号只有一个上色。 3、相邻的两个括号不能上相同的颜色,但是可以都不上色。 问括号 上色有多少种方案?答案对1000000007取模。

输入描述:

输入括号序列s。(2<=|s|<=700)

输出描述:

输出方案数。

输入样例:

(())

输出样例:

12

分析

本题出自 C o d e F o r c e s   149 D   C o l o r i n g   B r a c k e t s CodeForces\ 149D\ Coloring\ Brackets CodeForces 149D Coloring Brackets

由于不能复制自己的代码,这道题写的时候慢慢敲花了很多时间,考试时通过了七成的用例,后面在一个乘法前面加上long long强转了下,提交到CF上就AC了,真的血亏。考试时急着写下一道题就没有改了,小小的细节引起了失分。

考察区间DP,相对于其它的区间DP问题,难点有三:

  • 想到用区间DP求解
  • 状态转移排除不合法的状态
  • 状态划分时的分类讨论

本题一开始我并没有想到区间DP,开始用 f [ i ] [ j ] f[i][j] f[i][j]表示括号序列中第 i i i个字符染色成 j j j的方案数,然后发现在状态转移时,不仅要考虑第 i − 1 i-1 i1层的状态,还需要考虑与第 i i i个字符配对括号的状态,显然状态的转移不是线性的,而括号问题更符合区间DP的特征。

状态表示: f [ i ] [ j ] [ a ] [ b ] f[i][j][a][b] f[i][j][a][b]表示合法的括号序列 s [ i → j ] s[i \rightarrow j] s[ij]给左括号 s [ i ] s[i] s[i]染成颜色a,给右括号 s [ j ] s[j] s[j]染成颜色b的方案数。

对于一个合法的括号序列 s [ i → j ] s[i \rightarrow j] s[ij],可以分为以下两种情况:

  • s [ i ] s[i] s[i] s [ j ] s[j] s[j]是匹配的,嵌套式括号序列,比如 ( ( ) ) (()) (()) f [ i ] [ j ] [ a ] [ b ] f[i][j][a][b] f[i][j][a][b]可以由 f [ i + 1 ] [ j − 1 ] [ c ] [ d ] f[i+1][j-1][c][d] f[i+1][j1][c][d]状态转移而来。
  • s [ i ] s[i] s[i] s [ j ] s[j] s[j]不匹配,并列式括号序列,比如 ( ) ( ) ()() ()(),设与 s [ i ] s[i] s[i]匹配的右括号为 s [ k ] s[k] s[k] f [ i ] [ j ] [ a ] [ b ] f[i][j][a][b] f[i][j][a][b]可以由 f [ i ] [ k ] [ c ] [ d ] f[i][k][c][d] f[i][k][c][d] f [ k + 1 ] [ j ] [ e ] [ f ] f[k+1][j][e][f] f[k+1][j][e][f]状态转移而来。

由于状态转移方程写起来比较复杂,具体实现可以参考代码,我加上了详细的注释,不得不说这题不仅分类讨论的代码写起来冗长,状态转移用多重循环实现也挺费脑子的。

代码

#include <iostream>
#include <string>
#include <sstream>
#include <vector>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7,N = 705;
int f[N][N][3][3];
int m[N];
int solution(std::string s) {
	int n = s.size();
	vector<int> stk;
	for(int i = 0; i < n; i++) {//预处理一下括号匹配的位置
		if(s[i] == '(') stk.push_back(i);
		else {
			int u = stk.back();
			stk.pop_back();
			m[u] = i;
			m[i] = u;
		}
	}
	for(int i = 0; i < n; i++) {//状态边界
		if(m[i]==i+1) f[i][i+1][0][1] = f[i][i+1][0][2] = f[i][i+1][1][0] = f[i][i+1][2][0] = 1;
	}
	for(int len = 4; len <= n; len += 2) {
		for(int i = 0,j = i + len - 1; j < n; i++,j++) {
			if(s[i] != '(' || s[j] != ')') continue;
			if(m[i] == j) {
				for(int a = 1; a < 3; a++) {//只枚举需要染色的一端
					for(int b = 0; b < 3; b++) {
						if(b == a) continue;//匹配的括号颜色不能相同
						for(int c = 0; c < 3; c++) {
							f[i][j][a][0] = (f[i][j][a][0] + f[i+1][j-1][b][c]) % MOD;
							f[i][j][0][a] = (f[i][j][0][a] + f[i+1][j-1][c][b]) % MOD;
						}
					}
				}
			} else {
				int i1 = m[i],j1 = m[i] + 1;
				for(int a = 0; a < 3; a++) {
					for(int b = 0; b < 3; b++) {
						for(int c = 0; c < 3; c++) {
							for(int d = 0; d < 3; d++) {
								if(b == c && b) continue;//相邻括号有颜色不能相同
                                   //乘法原理,不用考虑子序列匹配的括号颜色是否相同,相同的话值就是0
                                   //这里乘法要加上强转,不然会爆int
								f[i][j][a][d] = (f[i][j][a][d] + ((ll)f[i][i1][a][b] * f[j1][j][c][d]) % MOD) % MOD;
							}
						}
					}
				}
			}
		}
	}
	int res = 0;
	for(int i = 0; i < 3; i++) {
		for(int j = 0; j < 3; j++) {
			res = (res + f[0][n-1][i][j]) % MOD;
		}
	}
	return res;
}
int main() {
	std::string s;
	std::cin>>s;
	int result = solution(s);
	std::cout<<result<<std::endl;
	return 0;
}

4.题目名称:喜水青蛙

题目描述

总是喜欢在水里嬉戏的青蛙,某天要过河拜访一位朋友。
已知河道中长满了带刺的不知名生物,能通过的路只有一条直线,长度为L。
直线上随机分布着m块石头。青蛙的最小跳跃距离是s,最大跳跃距离是t。
青蛙想要尽可能的少踩石头,那么它通过河道最少会踩到多少石头?

输入描述:

多组数据输入,其中每组数据:
第一行输入1个整数L(1<=L<=1e9)。
第二行输入3个整数:s、t、m(1<=s<=t<=10,1<=m<=100)。
第三行输入m个不同的整数,表示m个石子在数轴上的分布位置。
每行所有相邻整数用空格隔开。

输出描述:

输出青蛙过河最少会踩到的石子数量,
每组输入数据对应的输出结果单独成行。

输入样例:

10
2 3 5
2 3 5 6 7

输出样例:

2

分析

这道题知道性质难度是中等,不知道性质难度妥妥的hard,考试时最后二十分钟才做这道题,题意还理解错了。这道题的题意表述不太清楚,用例还没解释,相信也有不少人理解错了题意。乍一看题目,河道里长了刺,有m块石头,以为青蛙只能跳在石头上,就当作简单的DP去做了。实际上青蛙是可以跳在水中的,而且标题就叫喜水青蛙,说明青蛙更愿意跳进水中,而不愿意跳上石头。总之,题意是想让青蛙从数轴的0点出发,向右每次跳s到t的距离,直到跳到L以后,求最少能踩中的石头数量。题目源自NOIP2005提高组。

首先如果L的长度不超过一万,那么我们完全可以按照简单的DP去求解了。设 w [ i ] w[i] w[i]表示坐标是 i i i的位置有没有石头的状态。
w [ i ] = { 1 , 坐标 i 上有石头 0 , 坐标 i 上没有石头 w[i]= \begin{cases} 1, & \text{坐标$i$上有石头}\\ \\ 0,& \text{坐标$i$上没有石头} \end{cases} w[i]=1,0,坐标i上有石头坐标i上没有石头

  • 状态表示: f [ i ] f[i] f[i]表示跳到坐标 i i i踩到石头的最少数量。
  • 状态边界: f [ i ]   =   I N F f[i]\ =\ INF f[i] = INF f [ 0 ]   =   0 f[0]\ =\ 0 f[0] = 0
  • 状态转移方程: f [ i ]   =   m i n ( f [ i ] ,   f [ j ]   +   w [ i ] ) f[i]\ =\ min(f[i],\ f[j]\ +\ w[i]) f[i] = min(f[i], f[j] + w[i]),其中 i − t < = j < = i − s i-t<=j<=i-s it<=j<=is

但是题目L的范围是 1 0 9 10^9 109,直接DP不论是时间还是空间都承受不了。注意到虽然L的范围很大,但是石头的总数m不超过100,每次跳的距离也不超过10,这意味着如果两块石头之间距离比较大,那么很多中间状态是我们不需要关注的。由于每次跳的距离不超过10,所以我们只关注每个石头坐标前10个单位的状态,由于两个石头中间的状态不会增加石头数,所以只需要关注石头前面的一些坐标是否可达。假设 s = 5 , t = 6 s=5,t=6 s=5,t=6,从起点可以跳到5和6,然后从5和6又可以跳到10,11以及12.如果我们考虑每次跳s到t的距离,所有可达的地方,那么问题会很复杂。

首先引入数论的一个定理:

如果 a , b a,b a,b 均是正整数且互质,那么由 a x + b y , x ≥ 0 , y ≥ 0 ax+by,x≥0,y≥0 ax+by,x0,y0不能凑出的最大数是 a b − a − b ab−a−b abab

定理的证明过程可以参考 数 论 : p x + p y 不 能 表 示 的 最 大 数 为 p q − p − q 的 证 明 数论:px+py 不能表示的最大数为pq-p-q的证明 px+pypqpq

(我会说是证明过程太麻烦我懒得写了嘛)。

下面可以分两种情况讨论:

  • 如果 s = = t s==t s==t,说明青蛙每次跳的距离固定为 s s s,那么只要统计下有多少石头的坐标是可以整除 s s s的就知道跳到河对岸会踩到的石头数量了。
  • 如果 s   ! =   t s\ !=\ t s != t,说明每次可以选择跳 t t t或者 t − 1 t-1 t1步,由于 t t t t − 1 t-1 t1是互质的,根据上面的定理可得 p p p q q q不能表示的最大数是 ( p − 1 ) ( q − 1 ) − 1 (p-1)(q-1)-1 (p1)(q1)1,也就是不小于 ( p − 1 ) ( q − 1 ) (p-1)(q-1) (p1)(q1)的数都可以用p和q表示。 t t t的最大取值是10,也就是 p p p最多取10, q q q最多取9,所以不小于 ( 10 − 1 ) ( 9 − 1 ) = 72 (10-1)(9-1)=72 (101)(91)=72的数都可以用 t t t t − 1 t-1 t1来表示,我们取个整数 100 100 100,也就是只要第 i i i块和 i + 1 i+1 i+1块石头之间的距离超过一百,那么离第 i + 1 i+1 i+1块石头超过一百的距离都是没有意义的。举个例子,两块石头之间距离是100,说明第二块石头前10个单位一定是可达的;两块石头之间的距离是1000,第二块石头前10个单位也一定是可达的,所以我们可以将所有超过100的距离缩减为100.

最多不超过一百块石头,任意两块之间的距离不超过100,DP数组开到一万多一点,就可以AC本题了。至于枚举的状态,只需要枚举到最后一块石头的后10个单位即可,后面一定可以跳到对岸,也就是说,河道的长度L其实是用不到的。

最后,本题输入说是多组输入,给的输入模板是单组输入的模板,我就按照单组输入来实现本题了。

代码

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 10100;
int f[N],w[N],stones[105];
int main(){
    int L,s,t,m;
    cin>>L>>s>>t>>m;
    for(int i = 1;i <= m;i++)    cin>>stones[i];
    int res = 0;
    if (s == t) {
        for(int i = 1;i <= m;i++) {
            if(stones[i] % s == 0)  res++;
        }
        cout<<res<<endl;
    }
    else {
        int last = 0;
        sort(stones+1,stones+m+1);
        for(int i = 1;i <= m;i++) {
            int diff = min(stones[i] - last, 100);
            last = stones[i];
            stones[i] = stones[i-1] + diff;
        }
        for(int i = 1;i <= m;i++)   w[stones[i]] = 1;
        memset(f,0x3f,sizeof f);
        f[0] = 0;
        for(int i = s;i <= stones[m] + 10;i++) {
            for(int j = i - t;j <= i - s;j++) {
                if(j < 0)   continue;
                f[i] = min(f[i],f[j] + w[i]);
            }
        }
        res = 100;
        for(int i = stones[m];i <= stones[m] + 10;i++)  res = min(res,f[i]);
        cout<<res<<endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值