N枚硬币找1枚假币
――Neicole (2013.05.05)
0. 问题描述
共有N枚硬币,一个天平,在这N枚硬币中有一枚假币,设法找出该枚假币。
1. 原理示例(减治法)
![](https://img-blog.csdn.net/20130505171301575)
概要:
如上图所示,假设这次共有17枚硬币,其中,第4枚硬币为假币。
这里采用减治的思想,每次将硬币分为3组(天平组)+1组(余数组);
余数组是将硬币总数除3后的余数数目所组成的一组,余数组的数目可为0至2;
天平组是将硬币总数除以3所平均分出的每一组。
按此思路,估计找出假币的效率为O(log3n)。
详细解释:
第一步:初始化。我们需要将硬币按顺序排列起来,可以任意排;
第二步:分堆,称重。意思是,将这顺序排列的硬币组分为余数组和三个天平组。
由三个天平组称出重量不相同的一组为假币组,如果三组重量相同,则假币在余数组。
第三步:选堆。由刚刚的称重可以知道假币的位置,那么,此时选出该组,并做上标记。
循环步:循环第二步和第三步,直到标记的假币组的的数目小于3为止。
第四步:两两比较。由前面步骤可以从假币组外任意取一枚硬币作为真币,再将该真币与假币组中的每一个硬币比较,重量不相同的一个硬币即为假币,并且可以观察天平得出假币是重是轻。
2. 判断假币的算法伪码
3. 实验结果及分析
下面针对两个问题,分别做了测试,
1. 硬币数目恒定为5000,修改假币位置,程序的比较次数会有些什么变化。
2. 假币位置恒定为50,修改硬币的数目,程序的比较次数会有些什么变化。
我们先看第一个测试:硬币数目恒定为5000,修改假币位置,程序的比较次数会有些什么变化。
由上图可以看出,当硬币数目恒定为5000时,如果修改假币的位置,程序的比较次数基本上不会发生变化,在20的前后间浮动,而这浮动的原因,很有可能是因为当将它分为三组天平组和一组余数组时,天平组的重量都相等,直接跳出循环,使用余数组做最后的对比从而得出结果,因此此时比较次数就会相对较少。这里的算法设计余数组基本上是每轮循环对比后,顺序组靠前的位置进行分割。
我们可以再进行验算,分治次数log35000= 5,然后,每次分治使用三次天秤,就是5*3 =15,得出1~2个数时,再使用天秤进行最后一轮比较,再使用15+3 = 18, 15+3*2= 21,与图表结果接近。
得出的结果是:硬币数目恒定时,修改假币位置,程序的比较次数几乎不受影响。
我们看第二个测试:假币位置恒定为50,修改硬币的数目,程序的比较次数会有些什么变化。
由上图可以看出,当假币位置恒定为50,修改硬币的数目,程序的比较次数会呈阶级性地上升,在数字3, 9, 27, 81, 243, 729, 2187时,它们的比较次数都上升了3,由数字结果可以观察出它们都是3的n(n取1到无穷大)次方,这符合了log3N的规律,N增大3倍,比较次数就增多3次(即分治次数加1次)。
得出的结果是:假币位置恒定时,程序的比较次数随着硬币的数目的增大而呈增多,增多次数的规律符合log3N(N为硬币数目)。
4. 程序设计
概要:
本次程序实现使用Coins自定义类实现,其主要包含数据成员vector<char> B; 用于表示硬币组(n-1个真币,1个假币),char表示硬币重量,假币与真币的重量不相同。定义了嵌套类class Arrange,有数据成员int low及int height,表示区间[low, height],用于辅助硬币分组表示。
本次核心函数有三个:
private: balance(Arrange, Arrange)用于代表天秤,
private: int findDifferFromThree(Arrange, Arrange, Arrange, Arrange &)用于找出假币的范围,
public: int findDiffFromAll(int & weightCompare)用于外部调用,找出假币的位置。
具体原理已在前面提及到,下面是这三段代码的展示:
private: balance(Arrange, Arrange)
/**
* 函数名称:int Coins::balance(Arrange left, Arrange right);
* 函数功能:天平,比较两边范围的元素的总和的重量。
* 返回值: 1,左边重;0,同样重;-1,右边重。-2,范围错误,无法比较。
**/
int Coins::balance(Arrange left, Arrange right)
{
// 正确的范围判断
if(!(left.rightArrange() && right.rightArrange()) ) {
return -2;
}
// 求出sum1和sum2的总值
int sum1 = 0;
int sum2 = 0;
for(int leftIndex = left.low; leftIndex <= left.height; ++leftIndex) {
sum1 += static_cast<int>(B[leftIndex]);
}
for(int rightIndex = right.low; rightIndex <= right.height; ++rightIndex) {
sum2 += static_cast<int>(B[rightIndex]);
}
// 返回结果
if(sum1 > sum2) {
return 1;
}
else if(sum1 == sum2) {
return 0;
}
else{ // (sum1 < sum2)
return -1;
}
}
private: int findDifferFromThree(Arrange,Arrange, Arrange, Arrange &)
/**
* 函数名称:int Coins::findDifferFromThree(Arrange a, Arrange b, Arrange c, Arrange & res);
* 函数功能:从a, b, c三组硬币中,找出假币所在的组(假币的重量与真币的不同),将结果返回res中。
* 返回值: 如果不存在假币,res设为[0, 0],返回0;
* 如果存在假币,res设为假币所在范围,如果假币比真币重,返回1,如果比真币轻,返回-1。
* 如果a,b,c的组不存在,或者发生异常情况,返回-2.
**/
int Coins::findDifferFromThree(Arrange a, Arrange b, Arrange c, Arrange & res)
{
#ifdef __DEBUG_MODE__
// 需要使用天平两至三次
myClock.add(); // a,b
myClock.add(); // a,c
myClock.add(); // b,c
#endif
// 正确的范围判断
if(!(a.rightArrange() && b.rightArrange() && c.rightArrange())){
return -2;
}
// 开始对比:
// 全为真币,重量一样。 a == b == c, a == b && a == c
if(0 == balance(a, b) && 0 == balance(a, c)){
res.low = 0;
res.height = 0;
return 0;
}
// 假币存在于a中, a != b && b == c
if(0 != balance(a, b) && 0 == balance(b, c)){
res = a;
return balance(a, b);
}
// 假币存在于b中, a == c && a != b
else if( 0 == balance(a, c) && 0 != balance(a, b)){
res = b;
return balance(b, a);
}
// 假币存在于c中, a == b && a != c
else if( 0 == balance(a, b) && 0 != balance(a, c)){
res = c;
return balance(c, a);
}
// 存在其它情况
else{
return -2;
}
}
public: int findDiffFromAll(int &weightCompare)
/**
* 函数名称:int Coins::findDiffFromAll(int & weightCompare);
* 函数功能:找出这组硬币中的假币。
* 参数说明:weightCompare: 为0,没有假币,为1,假币重,为-1,假币轻。
* 返回值: 返回假币位置(下标+1),如果不存在假币,返回0,如果数组有误,返回-1
**/
int Coins::findDiffFromAll(int & weightCompare)
{
#ifdef __DEBUG_MODE__
myClock.start();
#endif
weightCompare = 0;
// 测试查找区间是否存在,超过三个硬币才有不同的一个硬币
if(B.size() < 3){
#ifdef __DEBUG_MODE__
OUT_RESULT_FILE();
#endif
return -1;
}
// 设置初始查找区间(此处采用闭合区间(针对数组下标) [low,height] )
Arrange arrNow(0, static_cast<int>(B.size() - 1) );
do{
// 按3组切割,三个组的硬币总数量需取3的倍数先将余数除出来,另作比较
int remainder = arrNow.size() % 3;
Arrange arrRemain(-1, -1);
if(0 != remainder){ // 存在余数
arrRemain.low = arrNow.low;
arrRemain.height = arrNow.low + remainder - 1;
arrNow.low = arrNow.low + remainder; // 重新设置需要划分的范围
}
// 划分出三个硬币组
int eachDiff = arrNow.size() / 3; // 求出每份大小
Arrange arrA(arrNow.low, arrNow.low + eachDiff - 1);
Arrange arrB(arrA.height + 1, arrA.height + eachDiff);
Arrange arrC(arrB.height + 1, arrNow.height);
// 组比较,并由函数设置下一次组的新范围
int findResVal = findDifferFromThree(arrA, arrB, arrC, arrNow);
if(0 == findResVal){ // 三组中不存在假币,假币可能存在于余数组中
arrNow = arrRemain;
}
}while(arrNow.size() >= 3);
if(-1 == arrNow.low){ // 分成三组,三组中没有假币,又没有余数零,则组中没有假币
#ifdef __DEBUG_MODE__
OUT_RESULT_FILE();
#endif
return 0;
}
// 先从该范围外找出一颗真币,(用于与这范围的硬币作比较)
int rightOne = 0;
for(int i = 0; i < static_cast<int>(B.size()); ++i){
if(i < arrNow.low || i > arrNow.height){
rightOne = i;
break;
}
}
// 此时在arrNow范围内的硬币剩下1颗(下标low)
for (int i = arrNow.low; i <= arrNow.height; ++i){
weightCompare = balance(Arrange(i,i), Arrange(rightOne,rightOne));
if(0 != weightCompare){ // 存在假币
#ifdef __DEBUG_MODE__
myClock.add();
OUT_RESULT_FILE();
#endif
return i + 1; // 返回结果为下标加1
}
}
#ifdef __DEBUG_MODE__
OUT_RESULT_FILE();
#endif
// 全部重量相等,不存在假币,返回0
return 0;
}
5. 完整代码
Coins.h
// Coins.h
/**
* 作者:Neicole
* 时间:2013.05.05
* 联系:http://blog.csdn.net/neicole
* 类名:Coins
* 成员变量:vector<char> B; 用于表示硬币组(n-1个真币,1个假币),char表示硬币重量,假币与真币的重量不相同
* 成员函数:构造函数 Coins(int size, int differIndex);size:硬币数量,differIndex:假币位置(由1开始计算)
* 找假币函数 int findDiffFromAll(int & weightCompare);具体使用方法,可查看(.cpp)函数说明。
**/
#ifndef _COINS_H_
#define _COINS_H_
#include <vector>
using std::vector;
// 硬币类(其中有N-1个真币,1个假币)
class Coins
{
private:
// 定义Arrange类,用于表示范围[low, height]
class Arrange
{
public:
int low;
int height;
Arrange():low(0), height(0) {}
Arrange(int l, int h):low(l), height(h){}
Arrange & operator = (const Arrange & sour){
low = sour.low; height = sour.height; return *this;
}
inline int size(){ return height - low + 1; }
inline bool rightArrange(){ return low >= 0 && low <= height; }
};
private:
vector<char> B; // 表示硬币组
private:
int balance(Arrange, Arrange); // 天秤,比较两边范围的重量
int findDifferFromThree(Arrange, Arrange, Arrange, Arrange &); // 找出假币的范围
public:
Coins(int size, int differIndex);
int findDiffFromAll(int & weightCompare); // 找出不同的元素(假币)
};
#endif // _COINS_H_
Coins.cpp
// Coins.cpp
/**
* 作者:Neicole
* 时间:2013.05.05
* 联系:http://blog.csdn.net/neicole
* 类名:Coins
* 成员变量:vector<char> B; 用于表示硬币组(n-1个真币,1个假币),char表示硬币重量,假币与真币的重量不相同
* 成员函数:构造函数 Coins(int size, int differIndex);size:硬币数量,differIndex:假币位置(由1开始计算)
* 找假币函数 int findDiffFromAll(int & weightCompare);具体使用方法,可查看(.cpp)函数说明。
**/
#define __DEBUG_MODE__
#include "Coins.h"
#ifdef __DEBUG_MODE__
#include "MyClock.hpp"
#include <fstream>
MyClock myClock;
#define OUT_RESULT_FILE() \
std::fstream ofile;\
ofile.open("testRes.txt", std::ios::out | std::ios::app); \
if(ofile){ \
ofile << myClock.getRunNum() << "\n"; \
ofile.close(); \
} \
myClock.end();
#endif
/**
* 构造函数:初始化硬币向量组。
* 参数说明:size:硬币数量,differIndex:假币位置(由1开始计算)
* 备注: 默认设置假币重量为3,真币重量为1;
* 如果假币位置不存在于硬币组中,则默认设置第一位为假币。
**/
Coins::Coins(int size, int differIndex):B(size, 1)
{
if(size> 0){
char badCoinWeight = 3;
if(differIndex >= 1 && differIndex <= static_cast<int>(B.size()) ){
B[differIndex - 1] = badCoinWeight; // 设置假币
}
else{ // 将第一位设为假币
B[0] = badCoinWeight;
}
}
}
/**
* 函数名称:int Coins::balance(Arrange left, Arrange right);
* 函数功能:天平,比较两边范围的元素的总和的重量。
* 返回值: 1,左边重;0,同样重;-1,右边重。-2,范围错误,无法比较。
**/
int Coins::balance(Arrange left, Arrange right)
{
// 正确的范围判断
if(!(left.rightArrange() && right.rightArrange()) ) {
return -2;
}
// 求出sum1和sum2的总值
int sum1 = 0;
int sum2 = 0;
for(int leftIndex = left.low; leftIndex <= left.height; ++leftIndex) {
sum1 += static_cast<int>(B[leftIndex]);
}
for(int rightIndex = right.low; rightIndex <= right.height; ++rightIndex) {
sum2 += static_cast<int>(B[rightIndex]);
}
// 返回结果
if(sum1 > sum2) {
return 1;
}
else if(sum1 == sum2) {
return 0;
}
else{ // (sum1 < sum2)
return -1;
}
}
/**
* 函数名称:int Coins::findDifferFromThree(Arrange a, Arrange b, Arrange c, Arrange & res);
* 函数功能:从a, b, c三组硬币中,找出假币所在的组(假币的重量与真币的不同),将结果返回res中。
* 返回值: 如果不存在假币,res设为[0, 0],返回0;
* 如果存在假币,res设为假币所在范围,如果假币比真币重,返回1,如果比真币轻,返回-1。
* 如果a,b,c的组不存在,或者发生异常情况,返回-2.
**/
int Coins::findDifferFromThree(Arrange a, Arrange b, Arrange c, Arrange & res)
{
#ifdef __DEBUG_MODE__
// 需要使用天平两至三次
myClock.add(); // a,b
myClock.add(); // a,c
myClock.add(); // b,c
#endif
// 正确的范围判断
if(!(a.rightArrange() && b.rightArrange() && c.rightArrange())){
return -2;
}
// 开始对比:
// 全为真币,重量一样。 a == b == c, a == b && a == c
if(0 == balance(a, b) && 0 == balance(a, c)){
res.low = 0;
res.height = 0;
return 0;
}
// 假币存在于a中, a != b && b == c
if(0 != balance(a, b) && 0 == balance(b, c)){
res = a;
return balance(a, b);
}
// 假币存在于b中, a == c && a != b
else if( 0 == balance(a, c) && 0 != balance(a, b)){
res = b;
return balance(b, a);
}
// 假币存在于c中, a == b && a != c
else if( 0 == balance(a, b) && 0 != balance(a, c)){
res = c;
return balance(c, a);
}
// 存在其它情况
else{
return -2;
}
}
/**
* 函数名称:int Coins::findDiffFromAll(int & weightCompare);
* 函数功能:找出这组硬币中的假币。
* 参数说明:weightCompare: 为0,没有假币,为1,假币重,为-1,假币轻。
* 返回值: 返回假币位置(下标+1),如果不存在假币,返回0,如果数组有误,返回-1
**/
int Coins::findDiffFromAll(int & weightCompare)
{
#ifdef __DEBUG_MODE__
myClock.start();
#endif
weightCompare = 0;
// 测试查找区间是否存在,超过三个硬币才有不同的一个硬币
if(B.size() < 3){
#ifdef __DEBUG_MODE__
OUT_RESULT_FILE();
#endif
return -1;
}
// 设置初始查找区间(此处采用闭合区间(针对数组下标) [low,height] )
Arrange arrNow(0, static_cast<int>(B.size() - 1) );
do{
// 按3组切割,三个组的硬币总数量需取3的倍数先将余数除出来,另作比较
int remainder = arrNow.size() % 3;
Arrange arrRemain(-1, -1);
if(0 != remainder){ // 存在余数
arrRemain.low = arrNow.low;
arrRemain.height = arrNow.low + remainder - 1;
arrNow.low = arrNow.low + remainder; // 重新设置需要划分的范围
}
// 划分出三个硬币组
int eachDiff = arrNow.size() / 3; // 求出每份大小
Arrange arrA(arrNow.low, arrNow.low + eachDiff - 1);
Arrange arrB(arrA.height + 1, arrA.height + eachDiff);
Arrange arrC(arrB.height + 1, arrNow.height);
// 组比较,并由函数设置下一次组的新范围
int findResVal = findDifferFromThree(arrA, arrB, arrC, arrNow);
if(0 == findResVal){ // 三组中不存在假币,假币可能存在于余数组中
arrNow = arrRemain;
}
}while(arrNow.size() >= 3);
if(-1 == arrNow.low){ // 分成三组,三组中没有假币,又没有余数零,则组中没有假币
#ifdef __DEBUG_MODE__
OUT_RESULT_FILE();
#endif
return 0;
}
// 先从该范围外找出一颗真币,(用于与这范围的硬币作比较)
int rightOne = 0;
for(int i = 0; i < static_cast<int>(B.size()); ++i){
if(i < arrNow.low || i > arrNow.height){
rightOne = i;
break;
}
}
// 此时在arrNow范围内的硬币剩下1颗(下标low)
for (int i = arrNow.low; i <= arrNow.height; ++i){
weightCompare = balance(Arrange(i,i), Arrange(rightOne,rightOne));
if(0 != weightCompare){ // 存在假币
#ifdef __DEBUG_MODE__
myClock.add();
OUT_RESULT_FILE();
#endif
return i + 1; // 返回结果为下标加1
}
}
#ifdef __DEBUG_MODE__
OUT_RESULT_FILE();
#endif
// 全部重量相等,不存在假币,返回0
return 0;
}
MyClock.hpp
// MyClock.hpp
#ifndef _MYCLOCK_
#define _MYCLOCK_
#include "Coins.h"
#include <ctime> // clock() 函数返回自程序开始运行的处理器时间
class MyClock
{
private:
friend class Coins;
private:
clock_t startTime; // 记录开始时间
clock_t endTime; // 记录结束时间
long runNum; // 记录运行次数
public:
MyClock::MyClock(){
clear();
}
inline int start(){
clear();
startTime = clock();
return startTime;
}
inline int end(){
endTime = clock();
return endTime;
}
inline int getRunTime(){
return static_cast<int>( (endTime-startTime)/CLOCKS_PER_SEC*1000.0);
}
inline int add(){
++runNum;
return runNum;
}
inline long getRunNum(){
return runNum;
}
void clear(){
runNum = 0;
startTime = 0;
endTime = 0;
}
};
#endif // _MYCLOCK_
main.cpp
/**
* 题目:设计减治算法实现n枚硬币中存在1枚假币的问题,假币可能比真币重或者比真币轻。
* 作者:Neicole
* 时间:2013.05.05
* 联系:http://blog.csdn.net/neicole
**/
#include "Coins.h"
#include <iostream>
#include <string>
using namespace std;
int testResult1000Group(); // 使用1000组数测试这算法的正确性
int differentIndex(); // 同样长度,不同下标下的测试
int differentLength(); // 同样下标,不同长度的测试
int main()
{
int testRes = testResult1000Group();
if(0 == testRes){
cout << "使用1000组数据做测试,算法验证成功\n\n";
}
differentIndex(); // 同样长度,不同下标下的测试
differentLength(); // 同样下标,不同长度的测试
system("pause");
return 0;
}
/******************************* 结果显示字符串 *****************************/
// 1,假币重;0,没假币;-1,假币轻
string weightResDisplay(const int & weightCompare)
{
string res;
switch(weightCompare){
case 1: {res = "假币比较重"; break;}
case -1: {res = "假币比较轻"; break;}
case 0: {res = "这里没假币"; break;}
default: {res = "有错误啦"; break;}
}
return res;
}
string weightIndexDisplay(int index)
{
if(-1 == index){
return "硬币数量不足,无法测出这组硬币的假币";
}
else if(0 == index){
return "这组硬币不存在假币";
}
else{
char charBuf[33];
return "假币在这组硬币的第" + string (itoa(index, charBuf, 10)) + "个";
}
}
/******************************* 测试代码 *****************************/
// 使用1000组数测试这算法的正确性
int testResult1000Group()
{
cout << "显示前15次的测试结果:\n";
for(int i = 0; i < 15; ++i){
cout << "i:" << i << "\t";
Coins * test = new Coins(i, 3); // 当假币位置不存在于硬币组(数量)中时,默认设第1个为硬币
int weightCompare = 0;
int resIndex = test -> findDiffFromAll(weightCompare);
cout << weightResDisplay(weightCompare) << "\t" << weightIndexDisplay(resIndex) << endl;
delete test;
}
cout << "31至1000组数的测试,不显示每组测试结果\n";
for(int i = 31; i < 1000; ++i){
Coins * test = new Coins(i, 15); // 当假币位置不存在于硬币组(数量)中时,默认设第1个为硬币
int weightCompare = 0;
int resIndex = test -> findDiffFromAll(weightCompare);
if(0 == resIndex){
cout << "假硬币不存于硬币组中,算法有误\n";
return -1;
}
delete test;
}
return 0;
}
// 同样长度,不同下标下的测试(运行结束时结果已输出到文件)
int differentIndex()
{
cout << "测试开始...\n";
for(int i = 1; i < 5000; ++i){
Coins * test = new Coins(5000, i); // 当假币位置不存在于硬币组(数量)中时,默认设第1个为硬币
int weightCompare = 0;
int resIndex = test -> findDiffFromAll(weightCompare);
delete test;
}
cout << "测试成功结束!\n";
return 0;
}
// 同样下标,不同长度的测试(运行结束时结果已输出到文件)
int differentLength()
{
cout << "测试开始...\n";
for(int i = 100; i < 5000; ++i){
Coins * test = new Coins(i, 50); // 当假币位置不存在于硬币组(数量)中时,默认设第1个为硬币
int weightCompare = 0;
int resIndex = test -> findDiffFromAll(weightCompare);
delete test;
}
cout << "测试成功结束!\n";
return 0;
}