CSDN竞赛57期题解

总结

交卷时一看才六十多分还有点吃惊,一看非编程题部分还是丢了二十分。填空题是这类竞赛最大的诟病,答案是名词的必然不唯一,答案需要计算的给定的参考答案必然计算错误,更离谱的是题目出成这样,反馈后官方竟然一点改变的意思都没有。但凡答案不唯一的题目你给多个候选答案也不会被人这么吐槽了。寄存器与状态寄存器、图灵机与图灵机模型这种不唯一的答案还记忆犹新,多少期前反馈时,C站不想着手动改一下判错的答案,而是回复以后不会再出填空题了,然而仅隔了一期填空题又再现了。

为什么这类竞赛的赞助商这么钟爱填空题呢?比如这次填空题答案是BST,看到题目时候我就在群里艾特出题人,BST至少有三种中文叫法,你想让我们填哪种?还特地翻了下书,没找着书上的叫法,于是在二叉搜索树、二叉排序树、二叉查找书当中选了个最常见的二叉搜索树填上去了,果不其然答案是二叉排序树,十分丢失。

另一道扣分的是判断题,问树的等价二叉树是不是唯一的,我寻思按照正常的转换方法应该是唯一的,结果却不是。失分的两题在书上也都没翻到解答。回到开始的问题,为什么赞助商总是出这种让人反感的填空题,他们的回复是以书上答案为准。提供了赞助,用这种方式迫使我们买本书不过分吧。还是随遇而安,叫不醒装睡的人,能白嫖本书也不亏。

编程题部分几乎该类竞赛每期都出现了不给数据规模的情况,可以发现出题人相当的不专业,python选手用map切分下就好了,C++选手每次写之前还要手动处理下输入数组,很是烦人。另外这次的两道题都不给数据范围,全靠自己猜范围了。

题目列表

1.凑数

题目描述

给定一组n个正整数,要求每次选其中一个数乘以或除以一个素数(称为一次凑数),问至少需要凑数多少次可以把所有的数都凑成相等。

分析

将所有数凑成相等第一反应是凑成最小公倍数或者最大公约数。但是这种方式的解很容易被推翻,比如2 4 8,凑成2和8的操作次数都是3,但是凑成4的次数是2,那么4这个数有什么性质呢?显然作为最终凑成的数,数组中小于它的数一定是它的约数,大于它的数一定是它的倍数。所以我们可以暴力求解一波,先列出一波候选解,除了数组里每个元素,还有就是它们的最大公约数和最小公倍数。可以先打表求出正向前 i i i个元素的最小公倍数 x x x以及逆向后 n − i n - i ni个元素的最大公约数 y y y,如果 x x x 能被 y y y整除就尝试将 x x x y y y作为候选解求下凑的次数,最后取个最小值就可以了。

当然比赛时我没有这么做,秉着最大得分原则,读完题就想着先混个部分分,求出所有数的最大公约数 t t t,然后求所有数到 t t t需要操作的次数之和。提交通过了大部分用例,数据范围改到十万再提交就AC了,可见这个题目的数据还是比较水的,有点数论基础的同学都可以比较快的AC。

代码

#include 
#include 
using namespace std;
int a[100005];
int gcd(int a, int b) {
	return b ? gcd(b, a % b) : a;
}
int get(int n) {
	int res = 0;
	for(int i = 2; i <= n; i++) {
		while(n % i == 0) {
			n /= i;
			res++;
		}
	}
	return res;
}
int main() {
	string s;
	getline(cin,s);
	int n = 0;
	int t = 0;
	int sz = s.size();
	for(int i = 0; i < sz; i++) {
		if (s[i] != ' ') t = t * 10 + s[i] - '0';
		else {
			a[n++]=t;
			t = 0;
		}
	}
	a[n++] = t;
	t = a[0];
	for (int i = 1; i < n; i++) {
		t = gcd(t, a[i]);
	}
	int res = 0;
	for(int i = 0; i < n; i++) {
		int r = a[i] / t;
		res += get(r);
	}
	cout<<res<<endl;
	return 0;
}

2.树的寻路

题目描述

给定一棵有n个节点且节点编号为1到n的树,求满足以下条件的路径组合数: 1. 从节点a到节点b的路径(称为路径ab) 边数为p 2. 从节点c到节点d的路径(称为路径cd)边数为q 3. 路径ab和cd不交,即不存在一个节点既在路径ab又在路径 cd上

分析

这类题目早几年做应该是可以秒掉的,工作躺平太久加之下班比较困了,花了挺长时间才通过四成用例。首先题目用例给了三条边,结果是8,没有用例说明还是让人很疑惑的,毕竟组合数这个词比较模糊。用例是1到2,2到3,3到4这三条边,乍一看不就1-2和3-4这两个长度为1的边不相交嘛,就算再倒过来算上3-4和1-2也才两种组合数啊,8是怎么得到的?不妨尝试去枚举一下路径的起点,第一条路径的起点可以是1 2 3 4,可以得到以下的路径组合:

  • 1-2,3-4
  • 1-2,4-3
  • 2-1,3-4
  • 2-1,4-3
  • 3-4,1-2
  • 3-4,2-1
  • 4-3,1-2
  • 4-3,2-1

虽然看起来这些组合有点离谱,就两条路径弄出了八个组合,但是用例来看应该就是这种组合方式了。

想象一下作为一棵树,其中的任意一个节点作为路径的起点,路径可以向其孩子节点延伸,也可以向其祖先节点延伸,也可以通过祖先节点向其兄弟、表兄弟节点延伸,我们选定一个根节点来遍历是没有什么意义的。

不妨先建个图,然后像上面枚举路径起点那样去枚举路径长度为 x x x路径的起点,遍历到其中一条长度为 x x x的路径的终点时,要保证遍历途中的节点做好了标记。再遍历下所有节点,只要没被标记的节点都可以作为与长度为 x x x路径不相交的、长度为 y y y的路径的起点。再次以新的起点去遍历相邻节点,直到找到与第一条路径不相交的路径,就将方案数加上1。

dfs过程中要注意遇见被标记的节点不可继续拓展,因为要么是第二条路径遍历的时候遇见的第一条路径上的节点,要么就是父节点,从 a a a走到 b b b,显然不能再走回来了。

这题没有给定数据范围,比赛时就假定数据范围是1w了,题目评测是一旦有超时的用例就是TLE,一分没有。所以加了下数据规模超过一千的就输出0。当然直接输出0也能得到一成的分数。比赛时由于时间关系没有继续调整数据范围和TLE的界限的,不然应该可以通过更多的用例。

代码

#include 
#include 
#include 
using namespace std;
const int N = 10005, M = 2 * N;
int idx = 0,e[M],ne[M],h[N];
int n,x,y,ans = 0;
int st[N];
void add(int a,int b) {
	e[idx]=b, ne[idx]=h[a],h[a] = idx++;
}
void dfs(int u, int d, int t) {
	if (!t) {//枚举的是第一条路径
		if (d == x) {//路径长度达到x
			for(int i = 1; i <= n; i++) {
				if (!st[i]) {//枚举第二条路径的起点
					st[i] = true;
					dfs(i, 0, 1);
					st[i] = false;
				}
			}
			return;
		}
	} else {//枚举的是第二条路径
		if (d == y) {//第二条路径长度达到y
			ans++;
			return;
		}
	}
	for(int i = h[u]; ~i; i = ne[i]) {
		int j = e[i];
        //已经被标记的节点跳过
		if (st[j]) continue;
		st[j] = true;
		dfs(j, d + 1, t);
		st[j] = false;
	}
}
int main() {
	cin>>n>>x>>y;
	memset(h,-1,sizeof h);
	for(int i = 1; i < n; i++) {
		int a,b;
		cin>>a>>b;
		add(a,b);
		add(b,a);
	}
	if (n > 1000) {
		cout<<0<<endl;
		return 0;
	}
	for(int i = 1; i <= n; i++) {
		st[i] = true;
        //枚举长度为x的路径起点i
		dfs(i, 0, 0);
		st[i] = false;
	}
	cout<<ans<<endl;
	return 0;
}

你可能感兴趣的:(其它,数论,dfs)