蓝桥杯 试题 算法训练 无聊的逗 C++ 详解 - 未完善

本文通过一个蓝桥杯算法题目的解题过程,详细介绍了如何使用深度搜索(DFS)解决将木棍分为两组,长度相等的问题。文章首先分析了特殊情况,然后逐步引入通用情况,并解释了DFS的思想和实现,包括递归参数的设计和递归过程中compare和add变量的变化。最终给出了C++代码实现。
摘要由CSDN通过智能技术生成

题目:

 

逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中。不过他想到了一个游戏来使他更无聊。他拿出n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,他想知道在两根一样长的情况下长度最长是多少。

输入格式:第一行一个数n,表示n个棍子。第二行n个数,每个数表示一根棍子的长度。

输出格式:一个数,最大的长度。

样例输入:4(回车) 

                  1(空格)2(空格) 3(空格)1(回车)

样例输出:3

数据规模和约定:n<=15


前言:

在上一篇文章完成后,我就赶着写下一题,但怎么都想不出来,看别人写的过程又是一知半解,甚至连题目提示的关键字(DP,搜索,状态搜索,贪心,动态规划)都不懂,于是决定学习算法,直到今天,突然想起再来看看这道题,看看有没有思路,然后莫名其妙就做出来了,然后又搜了一下CSDN现有的文章,关于这道题有

1、用 01 表示选或不选两种状态,进行穷举,暴力破解的:

试题 算法训练 无聊的逗_TheMARKZERO的博客-CSDN博客

2、(类比01背包问题)用动态规划的:

蓝桥杯 试题 算法训练 无聊的逗_BugMaker_7023的博客-CSDN博客

3、……等等等等

但发现这些文章讲的过程不是很详细,至少我之前看不太懂,于是打算分享一下自己的解题过程。


开始:(我用的是 DFS ,即深度搜索,就是用递归解决。不知道 dfs 的同学也不要紧,这仅仅是一个专业名称而已,重要是学习做题的思路,培养编程思维,我会详细地解释过程)

题目:n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,问在两根一样长的情况下长度最长是多少。

(就时刻想:怎么将一堆木棍分为两堆,这两堆木棍的总长度是一样长的? )

看到题,我第一时间想的是特殊情况:

1、如果 最长木棍的长度 == 所有木棍的总长度 ,直接输出结果,其他木棍全为一组即可。

2、如果 最长木棍的长度  >  所有木棍的总长度 ,不是无解,去除最长木棍,在其他木棍中继续。

接下来就是普遍情况了,虽然木棍们参差不齐,但没有特别长的情况。

然后就是要进行我们的DFS操作了,遍历所有可能,找出最优解。


谈谈我个人对DFS的理解:遇到选择的时候,先一条道走到底,直到不能再走的时候,返回上一级,再走另一道,直到走完所有道。

还是举例说明:现在有ABC三个物品,你可以不拿,可以拿一个、可以拿两个、还可以全拿。请列举出所有拿的方法。

我们先看A,此时面临两种选择:不拿0、拿1。假如我们不拿:0

接下来看B,还是面临两种选择:不拿0、拿1。我们依然不拿:00

最后是看C,还是面临两种选择:不拿0、拿1。我们还是不拿:000

第一种结果出来了,全不拿,000。 

(走到底了)接下来,(退回上一级)又是看C,面临两种选择:不拿0、拿1。这次我们拿:001

第二种结果出来了,仅拿C,001

以此类推……,还不太理解请看下图:

这就是 (双分支)深度搜索DFS ,黑色实心箭头每次都是一路到底才会停下, 再由黑色空心箭头返回上一级,继续走平行支路


继续解题:现在我们面临一堆木棍,利用 dfs ,可以遍历出所有的可能。

定义变量:compare 存放 被选择的木棍长度的总和,add 存放 未被选择的木棍长度的总和。

当遇到 compare == add 的时候,意味着,此时的选择是可以将木棍们分为两堆总和相等的。

但又有问题,有的同学可能已经遇到过,这样操作是会用上所有木棍的,但有时候如果全部木棍都用上,无论怎么排列都不可平均分为两堆呢? 例如:1 2 2 4 4,显然无论如何都不可能平均分。明显要去除掉一些木棍。

所以还要增加一步操作,如果所有木棍的总长度为奇数,那么就要去除最短的长度为奇数的木棍,才能进行 DFS ,因为 DFS 操作是对所有木棍进行各种组合的。

(注意:DFS操作是用上"所有木棍",将"所有木棍"进行各种组合的,所以务必将所有木棍的总长度置为偶数的情况下才能进行DFS操作,不然无法得到正确结果!!!)


综合:(具体操作)

1、获取数据(所有木棍长度),期间可以累加得总长度。

2、对数据进行降序排序(以找出最长木棍)。

3、判断:如果最长木棍 == 所有木棍总长度,输出结果,结束程序。

4、判断:如果最长木棍  >  所有木棍总长度去除之,继续。

5、判断:如果所有木棍总长度为奇数去除最短的长度为奇数的木棍

6、(关键) DFS 遍历操作:不断更新最优解。

7、最终输出结果。


关键:DFS怎么写?

首先作为递归,肯定要考虑参数。

必不可少的:数组(访问数据),元素个数(限制边界)

重点:我们知道递归是自己调用自己,而每次函数内进行的操作必然是相似的,而每次我们都要访问数组的"下一个元素",所以理所当然,有一个参数"下标",每次都会调用它的+1。

那么大体上,递归的结构就算完成了。但是现在仅仅是完成每次递归都可以访问不同元素而已,我们还需要进行一些操作。还记得上面提到的 compare add 吗?前者作为累加"被选择"的木棍总长度,后者作为累加"未被选择"的木棍总长度。

那么它们又应该怎样变化呢? 

首先,当该次递归访问到了某个元素(木棍),将面临两个选择: 或者 不要

如果要,那么 compare 是要 累加+= 该元素(木棍的长度)的,而 add 作为未被选择的元素(木棍)的总和,是要 -= 该元素(木棍的长度)的。

(同理)如果不要compate 不变,add 也不变。

脑海中想象,递归会有这样的变化,一开始:compare = 0,add = 总长度。因为每次都有选或不选的操作,将造成 compare不断变大,add 不断变小的情况,最终会有 compare = 总长度,add = 0,期间还有各种各样的情况。

但最最神奇的是,compare,add 作为参数传递下去,作为局部变量,是不影响当前函数内下一选择的。例如套用前面的案例的ABC,最开始我不选A,参数在BC的选与不选中逛了一大圈,到选A的时候,虽然是在不选A之后好多步才执行,但是当前的 compare 和 add 还是一样的。(当然,这里是我啰嗦了……)


附上代码:

#include<iostream>
using namespace std;
#include<algorithm>

//结果(作为"全局变量"可用于更新)
int maxLen = 0;

//深度搜索 - 选或不选
//(参数:add -> 未被选取木棍总长度,compare -> 被选取木棍的总长度,scan -> (下标)扫描位置)
void dfs(int ii[], int n, int add, int compare = 0, int scan = 0)
{
	//边界(若扫描位置越出数组了,意味着可以停止扫描了)
	if (scan >= n) return;

	//若出现 add == compare 则意味着当前选择可以平均分为两堆,且当前结果 > 历史结果,更新之
	if (add == compare && maxLen < compare) maxLen = compare;

	//选 - 此处自行体会,想通了下面这句,对了解本题非常关键
	dfs(ii, n, add - ii[scan], compare + ii[scan], scan + 1);

	//不选 - 不论选或不选,scan都应+1,往后扫描
	dfs(ii, n, add, compare, scan + 1);
}

int main()
{
	//记录所有木棍总长度
	int add = 0;
	int ii[15] = { 0 };

	//木棍个数
	int n; cin >> n;

	for (int i = 0; i < n; i++)
	{
		cin >> ii[i];
		add += ii[i];
	}

	//降序排序
	sort(ii, ii + n, greater<int>());

	//特殊情况:最长木棍长度等于总长度的一半(直接输出)
	if (ii[0] == add / 2.0)
	{
		cout << ii[0];
		return 0;
	}
	
	while (1)
	{
		//特殊情况:最长木棍长度大于总长度的一半(去除之)
		if (ii[0] > add / 2.0)
		{
			add -= ii[0];

			//移位的方式去除(以当前位置开始,后一位往前覆盖,即可实现删除功能)
			for (int i = 0; i < n - 1; i++)
			{
				ii[i] = ii[i + 1];
			}
			n--;
		}
		//特殊情况:木棍总长度为奇数,意味着用上所有木棍,无法平均分(去除最小奇数)
		if ((add & 1) == 1)
		{
			for (int i = n - 1; i >= 0; i--)
			{
				//(ii[i] & 1) == 1 证明是奇数,这是位运算的妙用,可以等价于 ii[i] % 2 == 0
				if ((ii[i] & 1) == 1)
				{
					add -= ii[i];
					for (int j = i; j < n; j++)
					{
						ii[j] = ii[j + 1];
					}
					n--;
					break;
				}
			}
		}
		else
		{
			break;
		}
	}

	//以上 while 内的操作,保证了所有木棍都被可用上,即可开始 dfs(深度搜索)

	//实参:n -> 木棍个数,add -> 所有木棍总长度
	dfs(ii, n, add);

	cout << maxLen;

	return 0;
}

重要!!修改:2022年03月01日

感谢网友"慕岁岁"的提醒,前面我所想的是:去除用不上的最小奇数木棍,使所有木棍总和为偶数,即可实现题意(主要是通过了测试,估计是系统测试数据不全,碰巧正确率100%而已)

但是请看这个案例:1 2 3 4 7,显然按照我原先的思路,去除1,剩余2 3 4 7,进行DFS,但是结果却是0,显然这样的情况下,用上所有木棍还是无法实现平均分的。

在不修改原先代码主体的前提下,我临时想的解决方法是:如果在之前的DFS操作后得到的结果为0,则再去除最小的木棍(不论奇偶),再来一次DFS。

即从原先代码的dfs(ii, n, add);处开始修改:

	//实参:n -> 木棍个数,add -> 所有木棍总长度
	while (maxLen == 0 && n >= 0)
	{
		dfs(ii, n, add);
		add -= ii[n];
		n--;
	}

	cout << maxLen;

 目前就先这样解决,可能还有不少案例可能是我所遗漏的,欢迎各位同学指正。


最关键的dfs函数,仅用四五行代码即可完成。这就是递归的妙用。

希望各位同学可以理解。

(下一题就又随缘了,现在还在学算法……2022.02.09)


更正!!!2022.03.25

蓝桥杯 试题 算法训练 无聊的逗 C++ 详解__Lyz_的博客-CSDN博客

  • 32
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Lyz_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值