时间限制:1.0s 内存限制:256.0MB
逗志芃在干了很多事情后终于闲下来了,然后就陷入了深深的无聊中。不过他想到了一个游戏来使他更无聊。他拿出n个木棍,然后选出其中一些粘成一根长的,然后再选一些粘成另一个长的,他想知道在两根一样长的情况下长度最长是多少。
第一行一个数n,表示n个棍子。第二行n个数,每个数表示一根棍子的长度。
一个数,最大的长度。
4
1 2 3 1
3
n < = 15 n<=15 n<=15
在这里我们最容易想到的解法就是枚举每一种情况来进行暴力求解,比较数据本身范围也不大
但是现在的难点在于如何表示某一种选木棒的情况,这时候就可以用二进制来进行存储了。
相信大部分同学的教务系统是有查询空教室的功能的,而这个查询空教室是可以选择任意周-星期-节次的任何排列的,他的原理就是用到了二进制
我们可以将一个数转化为二进制,而他二进制的每一位的1表示选,0表示不选,就比如说在这道题中,假设有四个木棍,我们选择第一个和第三个木棍,那么我们就可以将这种选择表示成二进制的0101.
而选择这两个木棍以外的其他所有木棍的情况就可以表示成1111-0101=1010
其次,我们想要判断一种选择的组合是否选择了某一个木棍a,我们只需要将这个木棍单独取出作为一种选择情况 1 < < ( a − 1 ) 1<<(a-1) 1<<(a−1),比如我选择了第二个木棍,那就是 1 < < ( 2 − 1 ) 1<<(2-1) 1<<(2−1)=0010.接着,我们将这种情况和要判断的选择组合进行按位与操作,如果结果为0说明当前选择没有选到这个木棍.
比如,1000这种选择情况,我们需要判断它是否选择了第二个木棍,那么我们只需要进行 1000 & 0010 1000\&0010 1000&0010这个运算,可以得出 0000 0000 0000=0,这时候我们就知道这种选择情况并不包含第二个木棍。
反之,1010这种情况, 1010 & 0010 = 0010 = 2 ≠ 0 1010\&0010=0010=2\not=0 1010&0010=0010=2=0,那这种选择情况是选择了第二个木棍的
接着再来说说,移位运算,比如1=0001,当他进行移位操作时,向左移两位,也就是 1 < < 2 1<<2 1<<2,得出来的结果就是 000100 = 0100 000100=0100 000100=0100也就是相当于 1 ∗ 2 2 1*2^2 1∗22,但是使用移位运算可以大大降低时间复杂度
还有,在下面一个代码中比较难理解的地方,就是对于两个木棍的序列,为什么第二个序列的排列情况需要跟第一个序列进行按位与操作,这个倒也不难解释
首先,假设我们序列1选择了2,3两个木棍,也就是00110,但是我们序列2枚举的是3,4,5三个木棍(11100),我们会发现存在一个木棍3重复使用了,这种情况肯定是不存在的,所以我们肯定要把第二个序列的木棍3去掉,想去掉,我们写循环加判断明显是太麻烦了,而且时间复杂度很高,所以我们可以使用按位与运算:假设我们现在2,3这两个棍子已经拿走,那么序列2只能选择(11001)这里面的木棍,我们将“可供选择的木棍列表”和“当前枚举的木棍序列2”进行按位与: 11001 & 11100 = 11000 11001\&11100 = 11000 11001&11100=11000,神奇不?是不是把多与的木棍3去掉了?
这个原理比较好理解,我们可以理解为,对于每一个木棍(也就是二进制的每一位),如果我当前有剩下的,并且我选到了他,那么我就把他选下,如果没有,我就不选他。
注释很全,多揣摩揣摩吧~
package com.lanqiao;
import java.util.Scanner;
/**
* @author 王宇哲
*/
public class 算法训练_无聊的逗 {
static int n;
static int[] nums;
/**
* 最长的相同木棍长度
*/
static int maxLength = Integer.MIN_VALUE;
/**
* 存储某种排列组合下木棍的长度
*/
static int[] length;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
n = scanner.nextInt();
nums = new int[n];
//对于n个木棍,共有2^n种排列组合方式
length = new int[1<<n];
for(int i=0;i<n;i++){
nums[i] = scanner.nextInt();
//1<
length[1<<i] = nums[i];
}
//遍历存储每一种选择方式的长度,i=1的情况
for(int i = 0;i < ( 1 << n ); i++){
//这一层遍历是用来选择木棍的
for(int j = 0;j < n;j++){
//如果i这种情况和选择木棍j有冲突,也就是第i种选择方式并没有选择第j跟木棍
//举个例子,比如i为4,二进制就是100,但是j为0,也就是选择了第1根木棍
//此时 1<
//因此这种情况出现冲突,不用进行运算
if((i & (1<<j) ) == 0){
continue;
}
//如果这种情况考虑在内了,那么第i种情况的长度就是木棍j的长度加上情况i不选择木棍j的长度
//还是举个例子
//比如目前i是5,也就是101,j是2,也就是第三根木棍,此时1<
//那么第i种组合的情况其实就是i = 001 时的长度加上木棍j的长度
length[i] = length[i - (1<<j)] + nums[j];
//这个break是用来防止重复计算的
break;
}
}
//枚举每一种情况i
for(int i=0;i< (1 << n);i++){
//j就是选出了所有情况i没有选择的木棍的情况,(1<
//1<
//1<
//(1<
//那么(1<
int j = (1 << n) -1 -i;
//接下来的循环是尝试从选择了全部i没选的木棍的情况开始枚举
//枚举每一种可能的选择情况,k--会存在不合理的数据,比如说现在剩下的木棍是11000,而k当前枚举到的实是10000,
//那么此时k-- = 01111,后面会多出三个木棍,所以我们需要使用按位与来去除多余的木棍,
for(int k = j;k > 0;k = (k-1)&j){
if(length[k] == length[i]){
maxLength = Math.max(maxLength,length[k]);
}
}
}
System.out.println(maxLength);
}
}