(2013.05.05)N枚硬币找1枚假币

N枚硬币找1枚假币

――Neicole (2013.05.05)

0. 问题描述

        共有N枚硬币,一个天平,在这N枚硬币中有一枚假币,设法找出该枚假币。

 

1. 原理示例(减治法)

概要:

如上图所示,假设这次共有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;
}


 

 

 

 

 

 

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值