CSDN竞赛7期题解

总结

这次竞赛的题目质量相对之前竞赛来说是有明显进步的,由两道经典面试题加上两道中等难度题目构成。前两道的受众可能是初学算法的同学吧,对于学算法的同学来说,前两道题没有在五分钟内AC都是不合格的。当然,偷懒这么久没学算法的我,也花了数倍的时间才ac前两道。

T3主要考察问题的分析能力,实现不难。T4考察数论基础,容斥原理和GCD,注意下细节也是不难ac的。

题目列表

1.奇偶排序

题目描述

给定一个存放整数的数组,重新排列数组使得数组左边为奇数,右边为偶数。

分析

经典面试题,由于思维惯性,往往第一反应是不使用额外空间,使用双指针来处理。但是最快的做法显然是使用额外空间,而且题目模板给的返回值已经是额外空间了。所以,不妨遍历两遍数组,第一遍将奇数加入vector,第二遍放入偶数。

有一点需要注意的是,本题没有说明重新排列后是否要维持奇偶元素内部的顺序,但是从用例看是要求维持相对顺序的。所以使用双指针的话也要注意维持奇偶元素内部的顺序。

代码

#include 
#include 
#include 
#include 
#include 
using namespace std;
std::vector<int> solution(int n, std::vector<int>& vec){
	vector<int> res;
	for(auto x : vec) {
		if(x & 1) res.push_back(x);
	}
	for(auto x : vec) {
		if(!(x & 1)) res.push_back(x);
	}
	return res;
}
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.小艺照镜子

题目描述

已知字符串str。 输出字符串str中最长回文串的长度。

分析

还是经典的面试题,问题的规模是1w,平方级别以内的算法都可以接受。

首先分析下暴力做法,二重循环分别来枚举子串的首尾位置,第三重循环来判断子串是否是回文串,时间复杂度是立方级别的。最方便的优化办法就是不去枚举子串的首尾位置,枚举子串的中间位置。

比如abba,以第一个a为中间的子串是a,以ab中间位置为中心的子串是ab,以下标为1的b为中心的子串有b,abb等等。

所以在遍历输入字符串时候,可以枚举以第i个字符为中心的和以i、i+1个字符中间位置为中心的最长回文子串的长度,最后取其中的最大值就是本题的解了。

当然本题还有更高效的线性时间复杂度的解法,可以参考AcWing 139 回文子串的最大长度。

代码

#include 
#include 
#include 
#include 
using namespace std;
int solution(std::string &s){
    int res = 0, n = s.size();
    for(int i = 0;i < n;i++) {
    	int p = i, q = i + 1;
    	while(p >= 0 && q < n && s[p] == s[q]) {
    		res = max(res, q - p + 1);
    		p--,q++;
    	}
    	p = i, q = i;
    	while(p >= 0 && q < n && s[p] == s[q]) {
    		res = max(res, q - p + 1);
    		p--,q++;
    	}
    }
    return res;
}
int main() {
    std::string s;
    std::cin>>s;
    int result = solution(s);
    std::cout<<result<<std::endl;
    return 0;
}

3. 交换后的or

题目描述

给定两组长度为n的二进制串,请问有多少种方法在第一个串中交换两个不同位置上的数字,使得这两个二进制串“或”的 结果发生改变?

分析

本题也是一类经典问题的变形,第一组二进制串上的一些位置的数字改变,可能会改变两个二进制串或的结果。

如果第二个二进制串对应位置上是1,第一个二进制串上对应位置上的数字不管怎么改变,最终或的结果都是1,所以能够让或的结果发生改变的,只能是第二个二进制串上为0的位置。第一个二进制串上的数是0,则与为1位置上的数交换下就可以改变该位置的或结果;第一个二进制串上的数是1,则与为0位置上的数交换下也可以改变结果。

举个例子:

10010011
01010100

第一个串有4个0和4个1,第二个串有5个0,并且5个0对应的第一个串位置上有3个1和2个0,我们改变这3个1或者2个0的数值都可以改变或的结果。改变1的数值可以去与第一个串中剩下4个0的位置交换,一共有3 * 4 = 12种交换方法;同理改变2个0的数值一共有2 * 4 = 8种方法。要注意的是,改变1的数值的交换方案中包含与2个0交换的方案,改变0的数值的交换方案中包含与3个1交换的方案,因此这部分是重复统计的,需要减去3 * 2 = 6个重复方案,因此,改变这两个串或结果的交换办法一共有12 + 8 - 6 = 14种。

设第一个二进制串上有a个0,b个1。第二个串中为0位置上对应第一个串的0的个数和1的个数分别是p和q,总的交换方案数就是aq + bp - pq。

代码

#include 
#include 
using namespace std;
int solution(int n, std::string str1, std::string str2){
    int a = 0,b = 0;//0 1
    for(int i = 0;i < n;i++) {
    	if(str1[i] == '1') b++;
    	else a++;
	}
    int p = 0,q = 0;
    for(int i = 0;i < n;i++) {
    	if(str2[i] == '0') {
    		if(str1[i] == '0') p++;
    		else q++;
    	}
    }
    int res = a * q + b * p - p * q;
    return res;
}
int main() {
    int n;
    std::string str1;
    std::string str2;
    std::cin>>n;
    std::cin>>str1;
    std::cin>>str2;
    int result = solution(n, str1, str2);
    std::cout<<result<<std::endl;
    return 0;
}

4.去除整数

题目描述

已知存在集合A包含n个整数,从1到n。 存在m个整数a[1…m]。 在集合A中去除这m个整数的倍数。 输出集合中包含 的元素的个数。

分析

本题考查容斥原理和最大公约数。数据范围n不超过109,m不超过10,但是注意要去除的是m个整数a[i]的倍数,虽然m不超过10,但是a[i]的范围可以很大。暴力做法很简单,遍历1到n中所有的数,对每个数判断下是否能够被a[i]整除,计算次数最多可以达到10 * 109 = 1010,所以显然是不可行的。

我们需要找的是1到n中去除m个整数的倍数剩下的数,也就是只要找到能被这m个数之一整除的数有多少个即可。而1到n中能被x整除的数一共有n / x个。举个例子更能说明问题。

n = 100,m = 3,a = [2, 3, 4]。100以内能被2整除的有50个,能被3整除的有33个,能被4整除的有25个,能否说明能被2或者3或者4整除的一共有50 + 33 + 25 = 108个呢?显然是不能的。设能被2整除的有x个,被3整除的有y个,被4整除的有z个,能被2和3整除的有a个,能被2和4整除的有b个,能被3和4整除的有c个,能被2,3,4整除的有d个,根据容斥原理可得,能被2或者3或者4整除的一共有x + y + z - a - b - c + d个。

现在问题来到如何求能被x和y同时整除的数的个数,比如能被2和3同时整除的数就是能被6整除的数,能否说明能被x和y同时整除的数的个数就是能被x * y整除的数的个数呢?显然不能,如果只计算能被x * y整除数的个数,只能通过七成左右的用例。再比如能同时被2和4整除的数一定能被4整除,所以实际上能同时被k个数整除的数的个数等于能被这k个数的最小公倍数整除数的个数。

总结下解题思路。例如m = 5,我们要求出1到n中能被五个数中的1个数整除的数的个数,能被5个数中2个数整除的数的个数,…。这就涉及组合数了,5个数中选1,2,3,4,5个的方案一共C(5,1)+C(5,2)+C(5,3)+C(5,4)+C(5,5) = 25种选法,我们可以用dfs将所有选法的最小公倍数存下,比如在2,3,4,5,6种选2个数可以是2,3,最小公倍数是6,能被6整除的一共n / 6个。1到n中能被m个数中一个数整除的一共x1个,能被2个数同时整除的一共x2个,能被3个数同时整除的一共x3个,…,所以最后能被这m个数中至少一个数整除的数一共有res = x1 - x2 + x3 - x4 +…个,n个数中去除res个数还剩n - res个数。

最后要注意求最小公倍数时的可能爆int,需要用long long存储,由于超过109的数不可能整除n,所以可以忽略最小公倍数超过109的方案。

有段时间没写算法题了,比赛时把gcd代码模板打错了,求lcm返回时也没有强转为long long,导致多花了不少时间才AC本题。

代码

#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;
const int N = 1e9 + 7;
vector<int> temp;
int gcd(int a,int b) {
    if(!b) return a;
    return gcd(b, a % b);
}
ll lcm(int a,int b) {
    if(a < b) swap(a,b);
    return (ll)a / gcd(a,b) * b;//计算时爆int,需要强转
}
//temp存储着v中选k个数的最小公倍数的集合
void dfs(vector<int>& v,int u,int k,int cnt,int s) {
    if(cnt == k) {//已经选了k个数
    	temp.push_back(s);
    	return;
    }
    if(cnt > k || u >= (int)v.size()) return;
    dfs(v,u+1,k,cnt,s);//不选第u个数
    ll t = lcm(s, v[u]);//选了第u个数后的最小公倍数
    if(t < N) dfs(v,u+1,k,cnt+1,t);//选第u个数
}
int solution(int n, int m, std::vector<int>& vec){
    int res = 0;
    for(int i = 1;i <= m;i++) {
    	temp.clear();
    	dfs(vec,0,i,0,1);//预处理选了i个数的最小公倍数的集合
    	if(i & 1) {
    		for(auto x : temp) res += n / x;
    	}
    	else {
    		for(auto x : temp) res -= n / x;
    	}
    }
    return n - res;
}
int main() {
    int n;
    int m;
    std::vector<int> vec;
    std::cin>>n;
    std::cin>>m;
    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));
    }
    int result = solution(n, m,vec);
    std::cout<<result<<std::endl;
    return 0;
}

你可能感兴趣的:(其它,容斥原理,dfs,gcd)