题目:
逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中。不过他想到了一个游戏来使他更无聊。他拿出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