八枚硬币问题
在八枚外观相同的硬币中,有一枚是假币,并且已知假币与真币的重量不同,但不知道假币与真币相比较轻还是较重。可以通过一架天平来任意比较两组硬币,设计一个高效的算法来检测出这枚假币。
我们先假设一个条件:已知假币比真币轻
二分查找算法实现:时间复杂度(O(log(以2为底n的对数)))
思路:把n枚硬币分成两堆,每堆有枚硬币,如果n为奇数的话,就留下一枚额外的硬币,然后把两堆硬币放在天平上。如果两堆硬币重量相同,那么放在旁边的硬币就是假币;否则我们可以用同样的方式对较轻的一堆硬币进行处理,这堆硬币中一定包含那枚假币。注意,即使我们把硬币分成了两个子集,但在每次称重之后,我们只需要解决一个规模为原来一半的问题。所以这是一个减治算法而不是一个分治算法。
public class Main {static int[] a = {2, 2, 2, 2, 2, 2, 1, 2};public static void main(String[] args) {int l = 0;int r = a.length-1;System.out.println(f2(l, r));}private static int f2(int l, int r) {int n = r - l + 1;int mid = (l+r) / 2;if(n == 1) {return l;}/*** n为总个数,mid为中值* n为偶数,则将n枚硬币分成两堆数量相等的硬币,对轻的一堆迭代称重* n为奇数,留下一枚硬币,对n-1枚硬币按偶数继续操作* */if (n % 2 == 0) {if (sum(l, mid) == sum(mid+1, r)) {return l - 1;} else if (sum(l, mid) < sum(mid+1, r)){return f2(l, mid);} else {return f2(mid+1, r);}} else {return f2(l+1, r);}}/*** 获取指定区域内硬币的重量* */private static int sum(int l, int r) {int sum = 0;for (int i = l; i <= r; i++) {sum += a[i];}return sum;}
}
思路:将n枚硬币分成三组,前两组有组硬币,其余的硬币作为第三组,将前两组硬币放到天平上,如果它们的重量相同,则假币一定在第三组中,用同样的方法对第三组进行处理;如果前两组的重量不同,则假币一定在较轻的那一组中,用同样的方法对较轻的那组硬币进行处理。这也是一个减治算法。
public class Main {static int[] a = {2, 2, 2, 2, 2, 2, 1, 2};public static void main(String[] args) {int l = 0;int r = a.length-1;System.out.println(f3(l, r));}private static int f3(int l, int r) {int n = r - l + 1;int x;if(n == 1) {return l;}/*** n为总个数* 将n分成n/3, n/3, n-2(n/3)三堆硬币* 对前两堆称重,相等则对第三堆继续操作,不相等则对轻的一堆继续操作* */if (n % 3 == 0) {x = n / 3;} else {x = n / 3 + 1;return f3(l+1, r);}int mid1 = l + x - 1;int mid2 = mid1 + x;if (sum(l, mid1) == sum(mid1+1, mid2)) {return f3(mid2+1, r);} else if (sum(l, mid1) < sum(mid1+1, mid2)){return f3(l, mid1);} else {return f3(mid1+1, mid2);}}/*** 获取指定区域内硬币的重量* */private static int sum(int l, int r) {int sum = 0;for (int i = l; i <= r; i++) {sum += a[i];}return sum;}
}
二分查找算法适用于单调的一个函数,即数组序列要么升序,要么降序
而三分查找算法使用于凸函数,常用来求极值问题。
在假币问题中,三分查找在n较大的情况下,效率是优于二分查找的。
最复杂的情况
下面来回到最开始的问题,在不知道假币轻重的情况下,我们通过下面的算法来实现,其时间复杂度为O(log(以2为底n的对数))
相比来说,这种情况思考起来比较复杂,但是好在逻辑清楚,只要理解了思路,算法还是好实现的,下面我们来举一个例子,假设有八枚硬币,其中有一枚硬币是假币。但是我们不知道假币是比真币重还是轻。先把八枚硬币编号,分别表示为a,b,c,d,e,f,g,h,从八枚硬币中任取六枚a,b,c,d,e,f,在天平两端各放三枚进行比较。假设a,b,c三枚放在天平的一端,d,e,f三枚放在天平的另一端,可能出现三种比较结果:
⑴ a+b+c>d+e+f
⑵ a+b+c=d+e+f
⑶ a+b+c<d+e+f
若a+b+c>d+e+f,可以肯定这六枚硬币中必有一枚为假币,同时也说明g、h为真币。这时可将天平两端各去掉一枚硬币,假设去掉c、f,同时将天平两端的硬币各换一枚,假设硬币b、e作了互换,然后进行第二次比较,比较的结果同样可能有三种:
① a+e>d+b:这种情况表明天平两端去掉硬币c、f且硬币b、e互换后,天平两端的轻重关系保持不变,从而说明了假币必然是a,d中的一个,这时我们只要用一枚真币(例如h)和a进行比较,就能找出假币。若a>h,则a是较重的假币;若a=h,则d为较轻的假币;不可能出现a<h的情况。(为什么?很简单,因为我们判断了a,d中有一个假币那么e,b都是真币,则e=d。而a+e>d+b可以推出a>d,所以不管a是真是假都不可能出现a<h情况出现)
② a+e=d+b:此时天平两端由不平衡变为平衡,表明假币一定在去掉的两枚硬币c,f中,同样用一枚真币(例如h)和c进行比较,若c>h,则c是较重的假币;若c=h,则f为较轻的假币;不可能出现c<h的情况。
③ a+e<d+b:此时表明由于两枚硬币b,e的对换,引起了两端轻重关系的改变,那么可以肯定b或e中有一枚是假币,同样用一枚真币(例如h)和b进行比较,若b>h,则b是较重的假币;若b=h,则e为较轻的假币;不可能出现b<h的情况。
public class Main {static int[] a = {2, 2, 2, 2, 2, 2, 1, 2};static int flag = 0;public static void main(String[] args) {int p;if (sum(0, 2) == sum(3, 5)) {p = fp(6, 7);} else if (sum(0, 2) > sum(3, 5)){if (a[0] + a[4] > a[3] + a[1]) {p = fp(0, 3);} else if (a[0] + a[4] == a[3] + a[1]) {p = fp(2, 5);} else {p = fp(1, 4);}} else {if (a[0] + a[4] > a[3] + a[1]) {p = fp(1, 4);} else if (a[0] + a[4] == a[3] + a[1]) {p = fp(2, 5);} else {p = fp(0, 3);}}if (flag == 1) {System.out.println("假币轻,为第" + p + "枚");} else {System.out.println("假币重,为第" + p + "枚");}}private static int fp(int l, int r) {int H, L;int x = (l+1) % 8;if(a[l] > a[r]) {H = l;L = r;} else {H = r;L = l;}if (a[H] > a[x]) {flag = 1;return H;} else {flag = -1;return L;}}/*** 获取指定区域内硬币的重量* */private static int sum(int l, int r) {int sum = 0;for (int i = l; i <= r; i++) {sum += a[i];}return sum;}
}