征求一算法. (看来都嫌分少,这样吧再加300,一共400了) (100分)

  • 主题发起人 主题发起人 LeeChange
  • 开始时间 开始时间
假如creation-zy兄的算法对的话,是否可以用互补法?
对于M是偶数,可有效翻动硬币为:-M,-M+2,...0,2,4,...,M
对于M是奇数,可有效翻动硬币为:-M,-M+2,...1,3,5,...,M
当剩余数目或N小到一定限度时,会减小翻动范围,于是用反向搜索法。
看来本题不一定无大规模解。[:)]
 
1)我对分数不感兴趣,我重新编辑的程序可以给出正确答案。
2)因为每个人叙述不一样,我理解题目的意思是:有 M = a + b 枚硬币,其中 a 枚正面朝上,b 枚反面朝上,每次只能翻动 n 枚,问最少多少次可以翻成全部某一面朝上,如果能,给出最少反动的次数,如果不能,给出不能的信息。
3)其实算法非常简单,当 n = 1 或者 a、b 之一可以被 n 整除等情况,谈不上用什么算法。在a、b 不是 n 的整数倍的情况下,我们尝试翻动 a 的 k 枚(k < = n ),这个时候 b 只能被翻动 n - k 枚,进行这样的操作之后:正反面朝上的硬币数分别是:a - k + ( n - k )、b - ( n - k ) + k,如果这两个数有一个是 n 的整数倍,答案就是: 倍数 + 1。这是我所说的算法,其实很多人知道是这样,但是有一个疑问,这个疑问是关于这样是否次数最少?
4)答案是肯定的。因为既然 a、b 之一不是 n 的整数倍,所以只能尝试翻动 k ( k <= n)枚硬币,有的人可能会问:是否存在若干次翻动,使得翻动次数最少?
翻动数字 状态
0 (a,b)
k1 (a + n -2*k1,b - n + 2*k1)
k2 (a + n -2*k2,b - n + 2*k2)
................................................
......................................................
ks (a + n -2*ks,b - n + 2*ks)
假设 b > a,s1 = [a/n],s2 = [b/n],所有的翻动操作可以转化成对下面的序列尝试翻动 k (k <= n)枚操作问题:
(a - s1*n,b + s1*n),(a - (s1 -1)*n,b + (s1 - 1)*n),...,(a + (s2 -1)*n,b - (s2 - 1)*n),(a + s2*n,b - s2*n)
有疑问的人实际上是问:上面的一对数是否有一个使得答案最小?可以看出这实际上是先翻动 n 枚的操作,程序给出的是后进行翻动 n 美的操作,翻动的总的次数是一样的.为了明显一点,我把上面的程序重新粘贴了过来,未作任何改动
//---------------------------------------------------------------------------
#include <math.h>
#include <iostream.h>
#include <conio.h>
#pragma hdrstop
//---------------------------------------------------------------------------
#pragma argsused
int main(int argc, char* argv[])
{
unsigned a,b,n,a1,b1,Num,x;
bool t;
char ch;
aa:
cout <<"a = ";
cin >>a;
cout <<"b = ";
cin >>b;
cout <<"n = ";
cin >>n;
for (int k = 0;k <=n;k++)
{
a1 = a + n - 2 * k;
b1 = b - n + 2 * k;
if(a1>=0&amp;&amp;b1>=0)
{
if(!fmod(a1,n))
{
t = true;
Num = a1 / n + 1;
x = k;
break;
}
else
if(!fmod(b1,n))
{
t = true;
Num = b1 / n + 1;
x = k;
break;
}
if(!fmod(a1,n)&amp;&amp;!fmod(b1,n))
{
t = true;
Num = b1 / n + 1;
x = k;
break;
}
}
}
if(x == 0)Num = 1;
if(t)cout << "Is true,"<<" Num = "<< Num<<" ,k = "<< x <<endl;
else
cout << "Is false!"<<endl;
ch = getch();
if(ch != 'q')goto aa;
return 0;
}
我还有一个进行验证的程序,想要的人留下地址
 
最令人头疼的是当 M趋近于 N的情况,我分析了很长时间,也未能找到规律,
在 M < Max(M,N-M) 的时候,到可以比较好的求解。(也不是100%可求)
根据上面我说的解空间概念,和一些其他比较容易分析到的结果,凑了一点代码如下:
const
MaxTimes = 300 ;
//最多300,小规模求解
var
//最终结果的步骤记录,为每向上硬币的数目,如果为零,表示被已知方法直接求出
ResultSteps:Array [0..MaxTimes-1] of Integer;
MiddleSteps:Array [0..MaxTimes-1] of Integer;
//中间结果的步骤记录
States:Array [0..MaxTimes-1] of Boolean;
//状态记录,防止震荡
//翻转硬币 N总数目,A硬币向上数目,M每次必须翻动数目 返回步数或者1达不到
function TurnCoins(N,A,M:Integer):Integer;
var
Is_M_Even:Boolean;
//M是偶数标志
MinTimes:Integer;
//最小次数,-1表示找不到
//保存当前最好结果
procedure SetCurBestResult(CurBestTimes:Integer);
begin
MinTimes:=CurBestTimes;
MoveMemory(@ResultSteps,@MiddleSteps,MinTimes*SizeOf(Integer));
end;

//获得某端的搜索次数
function GetSTT(Num:Integer):Integer;
begin
if (Num mod M)=0 //可以整除,可以直接出结果
then
Result:=Num div M
else
if ( ((Num mod M) mod 2)= 0 )=Is_M_Even
then
Result:=(Num div M) + 1 //整数倍后的余数在集合中,也可以直接找到
else
if Is_M_Even
then
Result:=-1 //偶数不可以找到
else
Result:=(Num div M) + 2;
//奇数需要加2次因为
end;
//设置中间步骤
procedure SetSTUpSteps(Times,Num:Integer);
var
i:Integer;
begin
if ( ((Num mod M) mod 2)=0) and (not Is_M_Even)
then
begin
Num:=Num-1;
Inc(Times);
MiddleSteps[Times]:=Num;
end;
if (Num mod M)<>0
then
begin
Num:=Num-(Num mod M);
Inc(Times);
MiddleSteps[Times]:=Num;
end;
for i:=1 to (Num div M)do
MiddleSteps[Times+i]:=Num-i*M;
end;
procedure SetSTDownSteps(Times,Num:Integer);
var
i:Integer;
begin
if ( ((Num mod M) mod 2)=0) and (not Is_M_Even)
then
begin
Num:=Num-1;
Inc(Times);
MiddleSteps[Times]:=N-Num;
end;
if ( Num mod M)<>0
then
begin
Num:=Num-( Num mod M);
Inc(Times);
MiddleSteps[Times]:=N-Num;
end;
for i:=1 to (Num div M)do
MiddleSteps[Times+i]:=N-(Num-i*M);
end;

//大范围情况的简单内搜索法
function SimpleTurn(UpNum,DownNum,Times:Integer):Integer;
var
UpTimes,DownTimes:Integer;
begin
UpTimes:=GetSTT(UpNum);
do
wnTimes:=GetSTT(DownNum);
if DWord(UpTimes)<=DWord(DownTimes)
then
begin
Result:=DWord(UpTimes);
SetSTUpSteps(Times,UpNum);
end
else
begin
Result:=DWord(DownTimes);
SetSTDownSteps(Times,DownNum);
end;
end;

//合适于大多数情况下的搜索
procedure Turn(UpNum,DownNum,times:Integer);
var
MinTurn, //可做到的下一状态最小变动有效硬币数(向上)
MaxTurn, //可做到的下一状态最大变动有效硬币数(向上)
CurTimes,
i:integer;
function Inpossible(Add:Integer):Boolean;
begin
Result:=DWord(Times+Add)>=DWord(MinTimes);
end;

procedure SelectSearch;
//选择搜索
begin
if Abs(MaxTurn)>Abs(MinTurn) //贪婪策略搜索--优先搜索两端,(不会相等,因M<N)
then
begin
Turn(UpNum+MaxTurn,DownNum-MaxTurn,times+1);
//大头开始
Turn(UpNum+MinTurn,DownNum-MinTurn,times+1);
end
else
begin
Turn(UpNum+MinTurn,DownNum-MinTurn,times+1);
//大头开始
Turn(UpNum+MaxTurn,DownNum-MaxTurn,times+1);
end;
i:=MinTurn+2;
while i<MaxTurndo
//依次搜索,惨,效率极度低下
begin
Turn(UpNum+i,DownNum-i,times+1);
if Inpossible(2) then
Break;
Inc(i,2);
end;
end;

begin
//以前搜索过该状态,退出
if States[UpNum] then
Exit;
MiddleSteps[Times]:=UpNum;
//记录当前状态
//以前的搜索结果比这好,退出
if DWord(Times)+1>=DWord(MinTimes) then
exit;
if ((UpNum>M*3)and(DownNum>M*3))or
(Is_M_Even and (UpNum>=M) and (DownNum>=M) )
then
begin
//简单查找,在3倍M以内或是偶数比一边小
CurTimes:=SimpleTurn(UpNum,DownNum,Times);
//O(1)复杂度
if (CurTimes>-1) then
begin
CurTimes:=CurTimes+Times;
if DWord(CurTimes)<DWord(MinTimes) then
SetCurBestResult(CurTimes);
end;
Exit;
end;

if DWord(Times)+2>=DWord(MinTimes) then
exit;
if UpNum<M //求最小变动有效
then
MinTurn:= M-2*UpNum
else
MinTurn:=-M;
ifdo
wnNum<M //求最大变动有效
then
MaxTurn:= 2*DownNum-M
else
MaxTurn:= M;
i:=MinTurn;
while i<=MaxTurndo
//先判断一下再说,复杂度O(M)
begin
if (UpNum+i=M)or(DownNum-i=M) then
//找到了,设置标志并退出
begin
CurTimes:=Times+2;
MiddleSteps[Times+1]:=UpNum+i;
if DWord(CurTimes)<DWord(MinTimes) then
SetCurBestResult(CurTimes);
Exit;
end;
Inc(i,2);
end;

States[UpNum]:=True;
//不允许重复该状态
SelectSearch;
States[UpNum]:=False;
//解除锁定
end;

begin
FillChar(ResultSteps,SizeOf(ResultSteps),0);
FillChar(MiddleSteps,SizeOf(MiddleSteps),0);
FillChar(States,Sizeof(States),0);
//均未访问过标志
//显然的情况
Result:=0;
if (A=0)or(A=N) then
Exit;
Result:=1;
if (M=A)or(M=N-A) then
exit;
//初始化
Result:=-1;
MinTimes:=-1;
Is_M_Even:= (M mod 2)=0;
//不可能找到合适解的情况
if M=0 then
Exit;
if N<=M then
Exit;
if (A mod 2=1)and( (N-A) mod 2=1)and Is_M_Even then
Exit;
//寻找
Turn(A,N-A,0);
Result:=MinTimes;
end;

调用举例:
procedure TForm1.Button1Click(Sender: TObject);
var
N,
i:Integer;
TurnTimes:Integer;
begin
ListBox1.Clear;
N:=SpinEdit1.Value;
TurnTimes:=TurnCoins(N,SpinEdit2.Value,SpinEdit3.Value);
if TurnTimes=-1
then
ListBox1.Items.Add('这是不可能翻到的!')
else
ListBox1.Items.Add(IntToStr(TurnTimes));
for i:=0 to TurnTimes-1do
ListBox1.Items.Add('向上硬币数目:'+IntToStr(ResultSteps) );
end;

如果有谬误,还请指正。
 
我用数学定理和程序搜索写出的程序可以在
小规模(比如N<=300)内求出全部解,时间一般在us级别,
但当规模扩大以后,虽然还可以快速求出一部分解,但另外
一些解因为搜索深度和宽度的增加而使复杂度呈现指数增长,
发生指数爆炸危机,因此不推荐使用.
代码如下:
------------------------------------------------------------------------
(* 求解翻硬币问题题
问题:
初始状态为:
A枚硬币朝上,N-A枚硬币向下;
硬币只有向上或向下两种状态
策略:
每次可翻动M枚硬币
目的:
使N枚硬币全部向上或向下
假设某一中间状态有t枚硬币朝上,N-t枚硬币向下
那么,翻动M枚后的有效翻动硬币x枚,所谓有效翻动指翻动前后两状态
的硬币向上数目之差: dt = t(i+1)-t(i) .
目标为:
选择合适的dt(1),dt(2),...dt(n)
使A+dt(1)+dt(2)+...dt(n)=0或N
使得n最小
分析:
所有可能的dt集合为:
{dt(1),dt(2),...dt(k)},显然有:任意 dt(i+1)-dt(i)=2
即他们是一个等差数列.
关于有效翻动数目 和 翻动向上、向下的硬币数目 关系为:
翻动M枚向上的,0枚向下的硬币有效翻动为-M,
翻动M-1枚向上的,1枚向下的硬币有效翻动为-M+2,
......
翻动0枚向上的,M枚向下的硬币有效翻动为M.
显然有一一对应关系,为了方便,只说有效翻动多少.
而且显然有: dt(i)+d(j)=dt(i+y)+d(j-y)
互补原理:
翻动相当于先全部翻转,然后再挑选N-M个进行翻转.
所以,只要翻转个数N-M存在最小路径: dt(1), dt(2), dt(3), dt(4)...
那么必然有N存在相补的最小路径: N-dt(1),dt(2),N-dt(3),dt(4)...
*)
unit DZTrunCoins;
interface
const
MaxTimes=300;
//最大可求数目
var
//成功走到的最终结果的步骤记录,为每向上硬币的数目
ResultSteps:Array [0..MaxTimes-1] of Integer;
//翻转硬币 N总数目,A硬币向上数目,M每次必须翻动数目
function TurnCoins(N,A,M:Integer):Integer;
// 成功则返回步数,翻不到时返回-1
implementation
uses Windows,SysUtils;
var
MiddleSteps:Array [0..MaxTimes-1] of Integer;
//中间结果的步骤记录
States:Array [0..MaxTimes-1] of Boolean;
//状态记录,防止震荡
function TurnCoins(N,A,M:Integer):Integer;
var
Is_M_Even:Boolean;
//M是偶数标志
i,
MinTimes:Integer;
//最小次数,-1表示找不到
//保存当前最好结果
procedure SetCurBestResult(CurBestTimes:Integer);
begin
if DWord(CurBestTimes)<DWord(MinTimes) then
begin
MinTimes:=CurBestTimes;
MoveMemory(@ResultSteps,@MiddleSteps,(MinTimes+1)*SizeOf(Integer));
end;
end;

//获得某端的搜索次数
function GetSTT(Num:Integer):Integer;
begin
if (Num mod M)=0 //可以整除,可以直接出结果
then
Result:=Num div M
else
if ( ((Num mod M) mod 2)= 0 )=Is_M_Even
then
Result:=(Num div M) + 1 //整数倍后的余数在集合中,也可以直接找到
else
if Is_M_Even
then
Result:=-1 //偶数不可以找到
else
Result:=(Num div M) + 2;
//奇数需要加2次因为
end;
//设置中间步骤
procedure SetSTSteps(Times,Num:Integer;Up:Boolean);
var
i:Integer;
begin
if ( ((Num mod M) mod 2)=0) and (not Is_M_Even)
then
begin
Num:=Num-1;
Inc(Times);
if Up
then
MiddleSteps[Times]:=Num
else
MiddleSteps[Times]:=N-Num;
end;
if (Num mod M)<>0
then
begin
Num:=Num-(Num mod M);
Inc(Times);
if Up
then
MiddleSteps[Times]:=Num
else
MiddleSteps[Times]:=N-Num;
end;
for i:=1 to (Num div M)do
if Up
then
MiddleSteps[Times+i]:=Num-i*M
else
MiddleSteps[Times+i]:=N-(Num-i*M);
if Up
then
MiddleSteps[Times+(Num div M)+1]:=0
else
MiddleSteps[Times+(Num div M)+1]:=N;
end;

//大范围情况的简单内搜索法
function SimpleTurn(UpNum,DownNum,Times:Integer):Integer;
var
UpTimes,DownTimes:Integer;
begin
UpTimes:=GetSTT(UpNum);
do
wnTimes:=GetSTT(DownNum);
if DWord(UpTimes)<=DWord(DownTimes)
then
begin
Result:=DWord(UpTimes);
SetSTSteps(Times,UpNum,True);
end
else
begin
Result:=DWord(DownTimes);
SetSTSteps(Times,DownNum,False);
end;
end;

//合适于大多数情况下的搜索
procedure Turn(UpNum,DownNum,times:Integer);
var
MinTurn, //可做到的下一状态最小变动有效硬币数(向上)
MaxTurn, //可做到的下一状态最大变动有效硬币数(向上)
CurTimes, //当前次数
i:integer;
//循环变量
function Inpossible(Add:Integer):Boolean;
begin
Result:=DWord(Times+Add)>=DWord(MinTimes);
end;

procedure SelectSearch;
//选择搜索
begin
Turn(UpNum+MinTurn,DownNum-MinTurn,times+1);
//用贪婪搜索策略从两端开始
Turn(UpNum+MaxTurn,DownNum-MaxTurn,times+1);
i:=MinTurn+2;
while i<MaxTurndo
//依次搜索,惨,效率极度低下
begin
if i<>0 then
Turn(UpNum+i,DownNum-i,times+1);
if Inpossible(2) then
Break;
Inc(i,2);
end;
end;

begin
//以前搜索过该状态,退出
if States[UpNum] then
Exit;
MiddleSteps[Times]:=UpNum;
//记录当前状态
//以前的搜索结果比这好,退出
if Inpossible(1) then
exit;
if ((UpNum>M*2)and(DownNum>M*2))or
(Is_M_Even and (UpNum>=M) and (DownNum>=M) )
then
begin
//在2倍M以外 或 是偶数的两端比M大,可用数学原理直接得出答案
CurTimes:=SimpleTurn(UpNum,DownNum,Times);
//O(1)复杂度
if (CurTimes>-1) then
//找到解了!
begin
CurTimes:=CurTimes+Times;
SetCurBestResult(CurTimes);
end;
Exit;
end;

//以前的搜索结果比这好,退出
if Inpossible(2) then
exit;
if UpNum<M //求最小变动有效
then
MinTurn:= M-2*UpNum
else
MinTurn:=-M;
ifdo
wnNum<M //求最大变动有效
then
MaxTurn:= 2*DownNum-M
else
MaxTurn:= M;
i:=MinTurn;
while i<=MaxTurndo
//先判断一下再说,复杂度O(M)
begin
if (UpNum+i=M)or(DownNum-i=M) then
//找到了,设置标志并退出
begin
//设置最后步骤
MiddleSteps[Times+1]:=UpNum+i;
if UpNum+i=M
then
MiddleSteps[Times+2]:=0
else
MiddleSteps[Times+2]:=N;
//设置最终结果
CurTimes:=Times+2;
SetCurBestResult(CurTimes);
Exit;
end;
Inc(i,2);
end;

States[UpNum]:=True;
//不允许重复该状态
SelectSearch;
States[UpNum]:=False;
//解除锁定
end;

begin
FillChar(ResultSteps,SizeOf(ResultSteps),0);
FillChar(MiddleSteps,SizeOf(MiddleSteps),0);
FillChar(States,Sizeof(States),0);
//均未访问过标志
//显然的情况
Result:=0;
if (A=0)or(A=N) then
begin
ResultSteps[0]:=A;
Exit;
end;
Result:=1;
if (M=A)or(M=N-A) then
begin
ResultSteps[0]:=A;
if M=A
then
ResultSteps[1]:=0
else
ResultSteps[1]:=N;
Exit;
end;

//初始化
Result:=-1;
MinTimes:=-1;
Is_M_Even:= (M mod 2)=0;
//不可能找到合适解的情况
if N>MaxTimes then
Exit;
if M=0 then
Exit;
if N<=M then
Exit;
if N<=A then
Exit;
if (A mod 2=1)and( (N-A) mod 2=1)and Is_M_Even
then
Exit;
//两端是奇而翻动是偶的情况
//寻找
if M*2<=N //概率分析域值
then
Turn(A,N-A,0)
else
begin
M:=N-M;
Is_M_Even:= (M mod 2)=0;
//从新设置奇偶性
Turn(N-A,A,0);
//互补搜索,采用一一对映可证明
i:=0;
while (i<=MinTimes)do
//必须对解进行互补修正
begin
ResultSteps:=N-ResultSteps;
Inc(i,2);
end;
end;

Result:=MinTimes;
end;

end.

--------------------------------------------------------------------------
应用举例:
procedure TForm1.Button1Click(Sender: TObject);
var
DS, //每两步之的向上硬币差值
N,
M,
i:Integer;
TurnTimes:Integer;
begin
ListBox1.Clear;
N:=SpinEdit1.Value;
M:=SpinEdit3.Value;
TurnTimes:=TurnCoins(N,SpinEdit2.Value,M);
if TurnTimes=-1
then
ListBox1.Items.Add('这是不可能翻到的!')
else
ListBox1.Items.Add('需要翻转步数:'+IntToStr(TurnTimes));
for i:=0 to TurnTimesdo
begin
ListBox1.Items.Add(Format('向上硬币数:%d 向下硬币数:%d',
[ResultSteps,N-ResultSteps] ) );
if i<TurnTimes then
begin
DS:=ResultSteps[i+1]-ResultSteps;
ListBox1.Items.Add(Format('step%d: 翻 %d下 %d上',
[i+1, (M-DS)div 2,M-( (M-DS)div 2) ]) );
end;
end;
end;

我在进行数学上推理时对如下几种情况分析:
对某状态下向上的硬币数量为t时有
1. t>=M , N-t>=M 时
2. (a) t>=M>N-t时
(b) N-t>=M>t时
3. M>t , M>N-t 时 (可转化为情况1)
其实上面的不少分析都忽略了情况2,3 -- 如果我考虑的情况还有疏忽,还请指教.
----但不知可否令楼主满意?(另外也想拜读一下楼主的程序)
 
DarwinZhange兄多虑了,我在出题的时候只想让大家用一般的广度优先来做,所以把空间问题限制死了,最长256.
 
LeeChange兄啊,为什么要限制成广度优先呢?无论什么方法都要以解决问题为目标啊?
而且广度优先实在是消耗空间复杂度(空间复杂度可比时间复杂度难分析得多).
我就个人而言,更多用基于深度搜索的思想.
何况算法我以为应该深入更深入的讨论才能有所收获.
我又发现实际上此问题实际上是可以求出中大规模的解的.但是方法比较繁杂.
----还是先看各位高手的解法吧.
 
我来一个,还不知道正确否,请各位高手指正(不管是算法,还是技巧):
object Form3: TForm3
Left = 236
Top = 111
Width = 695
Height = 416
Caption = 'Form3'
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -11
Font.Name = 'MS Sans Serif'
Font.Style = []
OldCreateOrder = False
PixelsPerInch = 96
TextHeight = 13
object Label4: TLabel
Left = 16
Top = 93
Width = 84
Height = 13
Caption = '背面向上的有:'
end
object Label3: TLabel
Left = 128
Top = 53
Width = 60
Height = 13
Caption = '翻动过程:'
end
object Label2: TLabel
Left = 16
Top = 139
Width = 60
Height = 13
Caption = '每次翻动:'
end
object Label1: TLabel
Left = 16
Top = 53
Width = 84
Height = 13
Caption = '正面向上的有:'
end
object Memo2: TMemo
Left = 126
Top = 69
Width = 551
Height = 268
ScrollBars = ssBoth
TabOrder = 0
end
object Memo1: TMemo
Left = 16
Top = 8
Width = 545
Height = 39
BorderStyle = bsNone
Color = clBtnFace
Ctl3D = False
Font.Charset = DEFAULT_CHARSET
Font.Color = clMaroon
Font.Height = -12
Font.Name = '宋体'
Font.Style = []
Lines.Strings = (
'说明:有N个硬币(1<=N<=255),有一些正面向上,另外一些背面向上,没有竖着的.可以对这些'
'硬币进行翻动,每次可以且仅可以翻动M个硬币(1<=M<=N).每次翻动必须翻M个硬币,不能多也不能少.'
'问最少翻多少次可以将所有硬币翻成同一面向上(既可以正面向上,也可以背面向上).')
ParentCtl3D = False
ParentFont = False
TabOrder = 1
end
object Edit3: TEdit
Left = 16
Top = 157
Width = 104
Height = 21
TabOrder = 2
Text = '10'
end
object Edit2: TEdit
Left = 16
Top = 109
Width = 104
Height = 21
TabOrder = 3
Text = '123'
end
object Edit1: TEdit
Left = 16
Top = 69
Width = 104
Height = 21
TabOrder = 4
Text = '108'
end
object Button1: TButton
Left = 40
Top = 312
Width = 75
Height = 25
Caption = '开始'
TabOrder = 5
OnClick = Button1Click
end
end
**********************************************************
unit Unit3;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;
Type
PTCoin = ^TCoin;
TCoin = record
m: integer;
n: integer;
k: Integer;
//翻正面的次数
o: integer;
Remark: string;
PParent: PTCoin;
end;

type
TForm3 = class(TForm)
Label4: TLabel;
Label3: TLabel;
Label2: TLabel;
Label1: TLabel;
Memo2: TMemo;
Memo1: TMemo;
Edit3: TEdit;
Edit2: TEdit;
Edit1: TEdit;
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
FPerCount: Integer;
EndPCoin: PTCoin;
procedure Calc(aPCoin: PTCoin);
function IsEnd(aPCoin: PTCoin): Boolean;
function GetNewCorn(aPCoin: PTCoin;
k: integer;
s: String): PTCoin;
function IsValidNode(aPCoin: PTCoin): Boolean;
function GetMinContNode(aPCoin: PTCoin;
Min, Max: integer): PTCoin;
function GetMin(aPCorn: PTCoin): integer;
public
{ Public declarations }
end;

var
Form3: TForm3;
implementation
{$R *.dfm}
procedure TForm3.Button1Click(Sender: TObject);
var
aPCoin: PTCoin;
TempPCoin: PTCoin;
str: String;
begin
aPCoin := new(PTCoin);
aPCoin^.m := StrToInt(Edit1.Text);
//正面
aPCoin^.n := StrToInt(Edit2.Text);
//反面
aPCoin^.k := 0;
aPCoin^.o := 0;
aPCoin^.Remark := '根';
aPCoin^.PParent := nil;
FPerCount := StrToInt(Edit3.Text);
//翻动个数
Memo2.Lines.Clear;
Memo2.Lines.Add('正面向上的' + Edit1.Text + ',' +
'背面向上的' + Edit2.Text + #13#10);
EndPCoin := nil;
Calc(aPCoin);
if EndPCoin <> nil then
begin
aPCoin := EndPCoin;
While aPCoin <> nildo
begin
str := Format('(%s) 正面%d;
反面%d;
翻正面的个数:%d;
次数:%d',
[aPCoin^.Remark, aPCoin^.m, aPCoin^.n, aPCoin^.k, aPCoin^.o]);
Memo2.Lines.Insert(0, str);
TempPCoin := aPCoin^.PParent;
Dispose(aPCoin);
aPCoin := TempPCoin;
end;
end;
end;

procedure TForm3.Calc(aPCoin: PTCoin);
var
NewPCoin: PTCoin;

k: integer;
//翻正面的个数
min, max: Integer;
//正面翻动的最小最大次数
TempMin: integer;
begin
if (aPCoin^.m = 0) or (aPCoin^.n = 0) then
begin
EndPCoin := aPCoin;
exit;
end;
if IsEnd(aPCoin) then
begin
if ((aPCoin^.m div FPerCount) = GetMin(aPCoin)) and
(aPCoin^.m mod FPerCount = 0) then
begin
NewPCoin := GetNewCorn(aPCoin, FPerCount, '靠近目标了(全部向下)...');
Calc(NewPCoin);
exit;
end
else
begin
NewPCoin := GetNewCorn(aPCoin, 0, '靠近目标了(全部向上)...');
Calc(NewPCoin);
exit;
end;
end
else
begin
if FPerCount - aPCoin^.n > 0 then
min := FPerCount - aPCoin^.n
else
min := 0;
if FPerCount - aPCoin^.m > 0 then
max := aPCoin^.m
else
max := FPerCount;
{for k := min to maxdo
begin
NewPCoin := GetNewCorn(aPCoin, k, '尝试...');
if IsEnd(NewPCoin) and IsValidNode(NewPCoin) then
begin
Calc(NewPCoin);
exit;
end
else
Dispose(NewPCoin);
end;
}
NewPCoin := GetMinContNode(aPCoin, min, max);
if NewPCoin <> nil then
begin
Calc(NewPCoin);
exit;
end;

//if FHadFind then
exit;
//不管怎么翻翻动一次不能成倍数
for k := min to maxdo
begin
NewPCoin := GetNewCorn(aPCoin, k, '搜索中...');
if not IsValidNode(NewPCoin) then
begin
Dispose(NewPCoin);
Continue;
end;
Calc(NewPCoin);
exit;
end;
end;
Memo2.Lines.Add('可能无解');
end;

function TForm3.IsEnd(aPCoin: PTCoin): Boolean;
begin
Result := (aPCoin^.m mod FPerCount = 0) or (aPCoin^.n mod FPerCount = 0);
end;

function TForm3.GetNewCorn(aPCoin: PTCoin;
k: integer;
s: string): PTCoin;
begin
Result := New(PTCoin);
Result^.m := (aPCoin^.m - k) + (FPerCount - k);
Result^.n := (aPCoin^.m + aPCoin^.n) - (aPCoin^.m - k) - (FPerCount - k);
Result^.k := k;
Result^.o := aPCoin^.o + 1;
Result^.PParent := aPCoin;
Result^.Remark := s;
end;

function TForm3.IsValidNode(aPCoin: PTCoin): Boolean;
var
TempPCoin: PTCoin;
begin
Result := False;
TempPCoin := aPCoin^.PParent;
While TempPCoin <> nildo
begin
//还了原,等于没有翻
if (TempPCoin^.m = aPCoin^.m) and (TempPCoin^.n = aPCoin^.n) then
exit;
//互补
if (TempPCoin^.m = aPCoin^.n) and (TempPCoin^.n = aPCoin^.m) then
exit;
TempPCoin := TempPCoin.PParent;
end;
Result := true;
end;

function TForm3.GetMinContNode(aPCoin: PTCoin;
Min, Max: integer): PTCoin;
var
k, n: integer;
NewPCoin: PTCoin;
begin
Result := nil;
for k := min to maxdo
begin
NewPCoin := GetNewCorn(aPCoin, k, '尝试...');
if IsEnd(NewPCoin) and IsValidNode(NewPCoin) then
begin
if Result = nil then
n := MaxInt
else
begin
n := GetMin(Result)
end;
if n > GetMin(NewPCoin) then
Result := NewPCoin;
end
else
Dispose(NewPCoin);
end;
end;

function TForm3.GetMin(aPCorn: PTCoin): integer;
begin
Result := 0;
if (aPCorn^.m mod FPerCount = 0) then
Result := aPCorn^.m div FPerCount;
if (aPCorn^.n mod FPerCount = 0) and
(aPCorn^.m > aPCorn^.n) then
Result := aPCorn^.n div FPerCount;
end;

end.
 
呵呵,本意是想让人用广搜的,深度用的太多了。
空间复杂度是大了点,所以做了256的限制。
 
不知道我这个是广,还是深,以前学的全还给老师了哈。
这个对不对,刚刚又改了几个小错误。
楼主能把你的发给我看看吗?
bb775885@yahoo.com.cn
 
学习呀!
楼主能把你的发给我看看吗?
misxjq@sina.com
高手很多的,很后悔当初上学时没好好学呀!
后悔并痛苦着!
 
我认为这个问题不适合用广度优先,以下.Pas的
program Untitled;
{$APPTYPE CONSOLE}
label aa;
var
a,b,c,a1,b1,k,x,num:Cardinal;
CanOrNot : boolean;
face : string;
ch : char;
begin
aa:
write('a = ');
readln(a);
write('b = ');
readln(b);
write('c = ');
readln(c);
CanOrNot := false;
if (c = 0)or(c > a + b)then
begin
writeln('Input error!');
goto aa;
end;
if(a mod c = 0)and(b mod c = 0)then
begin
if(a < b)then
Num := a div c
else
Num := b div c ;
CanOrNot := true;
end
else
if (a mod c = 0)then
begin
Num := a div c;
CanOrNot := true;
end
else
if (b mod c = 0)then
begin
Num := b div c;
CanOrNot := true;
end
else
for k := 0 to cdo
begin
a1 := a + c - 2 * k;
b1 := b - c + 2 * k;
if(a1 >= 0)and(b1 >= 0)then
begin
if( a1 mod c = 0)then
begin
CanOrnot := true;
Num := a1 div c + 1;
x := k;
face := 'b face';
break;
end;
if( b1 mod c = 0)then
begin
CanOrnot := true;
Num := b1 div c + 1;
x := k;
face := 'a face';
break;
end;
if( a1 mod c = 0)and(b1 mod c = 0)then
begin
CanOrnot := true;
if(a1 < b1)then
begin
Num := a1 div c + 1;
face := 'b face';
end
else
begin
Num := b1 div c + 1 ;
face := 'a face';
end;
x := k;
break;
end;
end;
end;
if(CanOrNot)then
begin
writeln('Can finish.');
writeln('The operate num is ',Num);
writeln('The up face is ',face);
writeln('First,we let k = ', x );
write('then
a = ',a);write(' + ',c);write(' - 2 * ',x);writeln(' = ',a + c - 2 * x);
write(' b = ',b);write(' - ',c);write(' + 2 * ',x);writeln(' = ',b - c + 2 * x);
writeln('then
a / c = ',(a + c - 2 * x) / c:4:4);
writeln(' b / c = ',(b - c + 2 * x) / c:4:4);
writeln('So that, the total operate num is ',Num);
end
else
write('Cannot finish!');
read(ch);
if(ch <> 'q')then
goto aa
end.

 
温故而知新。昨天再看到这个题目又想了想,发现还是有点意思的。
LeeChange,如果下面的回答还满意的话,就给分了吧。
下面给出三种算法解答这个问题。
第一、广度优先遍历法(楼上认为不可用广度的可以看看)
第二、数学规律直接找最小过程解(速度极快但可能存在隐患问题)
第三、队列(也是一层层求解,类似广度,不过效率很好)
同时,在验证三种算法的正确性时,发现楼主LeeChange给的算法有bug
LeeChange,您的算法不能正确解出2,6,4(2上6下翻4个)等组合(其他几个不记得了)
记a,b,m(a上b下,翻m个)
算法一:利用Tree实现广度优先
优点:层次分明,代码简炼,准确易懂
缺点:当a且b除以m的倍数较大时,速度较慢,但可以同时让a,b减去m的i次和再计算,可以
提高相当可观的效率,本例中未实现这个优化。
算法二:数学规律直接找最小过程解
优点:速度一流,一般递归三次即可找到倍数
缺点:不严密,可能存在无法考虑到的情况
算法三:队列求解(是我的一个同学用java写的)
优点:利用队列特性,巧妙的实现逐层求解。
缺点:队列占用间开销比较大
以下为代码,附有详细原理说明和实现说明:
算法一:利用Tree实现广度优先
type
TPath = set of 0..255;
var
Path: TPath;
//a或b的变化范围
a, b, m: Integer;
Succ: Boolean;
{ 增加子结点 }
procedure AppendChildNode(Level: Integer);
var
i, x, y, L, Count: Integer;
Path: TPath;
S: string;
N: TTreeNode;
procedure GetPath(Node: TTreeNode);
begin
Path := [a, b];
while Node.Level > 0do
begin
Include(Path, StrToInt(Node.Text));
Include(Path, a+b-StrToInt(Node.Text));
Node := Node.Parent;
end;
end;
begin
Count := Form1.Tree.Items.Count;
for L := 0 to Count-1do
begin
N := Form1.Tree.Items[L+Form1.Tree.Items.Count-Count];
if N.Level = Level then
begin
GetPath(N);
//不重复搜寻已用过的结点
x := StrToInt(N.Text);
y := a+b-x;
for i := mdo
wnto 0do
if not (x-i+m-i in Path) and (x>=i) and (y>=m-i) then

begin
Form1.Tree.Items.AddChild(N, IntToStr(x-i+m-i));
if x-i+m-i in [0, m, a+b-m, a+b] then
//谁的结点先找到谁就是最优解
begin
Succ := True;
while N.Level > 0do
begin
S := '-(' + N.Text + ',' + IntToStr(a+b-(StrToInt(N.Text))) + ')' + S;
N := N.Parent;
end;
S := '(' + IntToStr(a) + ',' + IntToStr(b) + ')' + S;
if x-i+m-i in [m,a+b-m] then
S := S + '-('+IntToStr(m)+','+IntToStr(b)+')';
S := S + '-(0,' + IntToStr(a+b) + ')';
Form1.Memo1.Lines.Add(S);
Break;
end;
Include(Path, x-i+m-i);
Include(Path, y+i-m+i);
end;
if Succ then
Break;
end;
end;
end;

procedure SetTree(N: TTreeNode);
begin
if (N <> nil) and not Succ then
begin
AppendChildNode(N.Level);
//增加子结点
SetTree(N.GetFirstChild);
//搜寻下一结点
end;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
a := StrToInt(Edit1.Text);
//上
b := StrToInt(Edit2.Text);
//下
m := StrToInt(Edit3.Text);
//翻
Succ := False;
SetTree(Tree.Items.Add(nil, inttostr(a)));
end;

算法二:数学规律直接找最小过程解
原理:
1. a不管如何变化只能变为[0, a+b]中的某个值,设为a1,a2,a3...,且a,b等同于b,a
2. 将a,b变过的值保存在Path中,下次如果a,b在Path中,就不处理a,b的这个变化。
3. 检查a或b每次变完成的值是否可以再变一次就成为m的倍数,如果可以,并且倍数<3或m是偶数,就Succ=True,
否则a继续变。(为什么要倍数<3呢?因为有可能另一个虽然不是m的倍数,但转换一或两次就可成为m的倍数,
为什么要或m是偶数呢?因为如果m是偶数,另一个数不管怎么变都不可成为m的倍数了)
4. 递归结束后,a,b肯定有一数是m的倍数,依次让a-m或b-m就可达到0
5. 由于1和2,因此递归遍历很快去除了所有重复变化;由于3, 递归遍历很快就到达了4,因此,
只遍历了极小部分就找到了最小解,效率应该是很高的。也因为3,这个算法应该就不能算是遍历算法:)
顶多只能算是递归找最小解的算法,因为每次a,b变化后的值就是过程解!!更无需回溯遍历过程。
实现:
1. 将a,b传入递归函数Turn(a,b)中,如果a>b,则传入Turn(b,a),以保证最小倍数处理时得到最小值
2. 在递归中 a). 保存a和b的值到Path中,下次不处理a或b变为Path中的值的情况
b). a每次都有[0, a+m]种变化,因此有 for i := mdo
wnto 0 处理每次变化
c). 在for j := ido
wnto 0do
中限制了a,b是朝着最优的方式变化的(原理3),因此,
a,b变化的值就是最小解过程,可以直接显示出a,b的值来做为答案。
d). a-j+m-j或a-i+m-i就是a下一个变化值
}
type
TPath = set of 0..255;
var
Path: TPath;
//a或b的变化范围
a, b, m: Integer;
Succ: Boolean;
procedure Turn(a, b: Integer);
var
i, j, k, t1, t2: Integer;
begin
if Succ then
Exit;
Form1.Memo1.Lines.Add(IntToStr(a) + ',' + IntToStr(b));
Include(Path, a);
Include(Path, b);
//因为(a,b)和(b,a)没什么区别,所有就不处理a变成b的情况了。
Succ := (( (a mod m = 0) and ( (a div m<2)or(m mod 2=0)and((a+b) mod 2=1) )) or
( (b mod m = 0) and ( (b div m<2)or(m mod 2=0)and((a+b) mod 2=1) )));
for i := mdo
wnto 0do
begin
for j := ido
wnto 0do
//决定a,b每次都是朝最优方式(即可能成为倍数)的方向变化。
if (a-j+m-j) mod m = 0 then
if (a>=j) and (b>=m-j) and not (a-j+m-j in Path) and (((a-j+m-j) div m <2) or (m mod 2=0) and ((a+b) mod 2=1)) then
Turn(a-j+m-j, b+j-m+j)
else
else
if (b-j+m-j) mod m = 0 then
if (b>=j) and (a>=m-j) and not (b-j+m-j in Path) and (((b-j+m-j) div m <2) or (m mod 2=0) and ((a+b) mod 2=1)) then
Turn(b-j+m-j, a+j-m+j);
for k := ido
wnto 0do
begin
t1 := a-k+m-k;
t2 := b+k-m+k;
if (a>=k) and (b>=m-k) then
for j := ido
wnto 0do
begin
if (t1-j+m-j) mod m = 0 then
if (t1>=j) and (t2>=m-j) and not (t1-j+m-j in Path) and (((t1-j+m-j) div m <2) or (m mod 2=0) and ((a+b) mod 2=1)) then
Turn(t1, t2)
else
else
if (t2-j+m-j) mod m = 0 then
if (t2>=j) and (t1>=m-j) and not (t2-j+m-j in Path) and (((t2-j+m-j) div m <2) or (m mod 2=0) and ((a+b) mod 2=1)) then
Turn(t2, t1);
end;
end;
if not Succ then
if (a>=i) and (b>=m-i) and not (a-i+m-i in Path) then
Turn(a-i+m-i, b+i-m+i);
end;
end;

procedure TForm1.Button1Click(Sender: TObject);
procedure ListEx(x: Integer);
begin
while x>0do
begin
Dec(x, m);
Memo1.Lines.Add(IntToStr(x) + ',' + IntToStr(a+b-x));
end
end;
begin
Memo1.Clear;
a := StrToInt(Edit1.Text);
//上
b := StrToInt(Edit2.Text);
//下
m := StrToInt(Edit3.Text);
//翻
if (m mod 2=0) and (a mod 2=1) and (b mod 2=1) or (a+b<m) or (a+b=m) and (a*b<>0) then
Exit;
if a > b then
begin
a := a+b;
b := a-b;
a := a-b;
end;
if m>a+b-m then
m := a+b-m;
Path := [];
Succ := False;
Turn(a, b);
b := a+b;
a := StrToInt(Copy(Memo1.Lines[Memo1.Lines.Count-1], 1,
Pos(',', Memo1.Lines[Memo1.Lines.Count-1])-1));
b := b-a;
if (a mod m=0) and ((b mod m<>0) or (b mod m=0) and (b>a)) then
ListEx(a) else
ListEx(b);
end;

算法三:队列求解
/*********************************************************************
问题描述:
有N个硬币(1<=N<=255),有一些正面向上,另外一些背面向上,没有竖着的.可以对这些
硬币进行翻动,每次可以且仅可以翻动M个硬币(1<=M<=N).每次翻动必须翻M个硬币(硬
币不同),不能多也不能少. 问最少翻多少次可以将所有硬币翻成同一面向上(既可以
正面向上,也可以背面向上).
输入:
字符串s,由字符'0'或'1'构成,'0'表示背面向上,'1'表示正面向上.
整型M,表示一次翻动的硬币的数目.
输出:
每次翻动后的字符串,直到字符串为全'0'或全'1'.
如果无解,则提示用户无解.
**********************************************************************/
/**
Demo program
Date:2003-6-11
Author:wcxlily
Note:Everyone can modify this program.
Modify:
2003-6-11 15:15 修改输出每次翻动的方法。
2003-6-11 17:15 修改程序isIntegerTimes()中的一个逻辑错误。
*/
import java.util.*;
import java.io.*;
/* 每次翻动后硬币的状态 */
class Node
{
/*Default constructor()*/
public Node(){ upCounter =do
wnCounter = prev = -1;
status = -1;
}

/*Copy constructor()*/
public Node(Node node)
{
up = node.up;
do
wn = node.down;
times = node.times;
upCounter = node.upCounter;

do
wnCounter = node.downCounter;
prev = node.prev;
status = node.status;
}

public boolean equals(Object o) {
if (!(o instanceof Node))
return false;

Node node = (Node)o;

return (up == node.up &amp;&amp;
down == node.down
&amp;&amp;
times == node.times
&amp;&amp;
upCounter == node.upCounter
&amp;&amp;
downCounter == node.downCounter
&amp;&amp;
prev == node.prev
&amp;&amp;
status == node.status);


}
//确定节点是朝上的或者朝下的是否是M的整数倍,如果是的话就返回倍数,
//并记录下要朝上还是朝下翻,如果不是的话返回-1
public int isIntegerTimes(int M)
{
int upTimes,do
wnTimes;

//朝上的为M的倍数
if(up - M == 0) {
status = 1;
return up/M;
}
//朝下的为M的倍数
if(down - M == 0){
status = 0;
returndo
wn/M;
}

status = -1;
return -1;
}
public String toString()
{
String str;

str = times + " Up = " + up + "do
wn =" +do
wn
+ " upCounter = " + upCounter
+ "do
wnCounter = " +do
wnCounter
+ " Prev = " + prev
+ " Status =" + status;
return str;
}

int up;
//硬币朝上的个数
intdo
wn;
//硬币朝下的个数
int times;
//在进行这次翻动的时候,前面已经翻动的多少次了
int upCounter;
//用于记录前一次到这一次,向上翻了多少个硬币
intdo
wnCounter;
//用于记录前一次到这一次,向下翻了多少个硬币
int prev;
//我用另外的Vector保存遍历信息,这个仅为了找
//回翻动硬币这次硬币的前一次在Vector中的索引而已
int status;
//如果朝上或者/和朝下为M的倍数的话,要向下翻还是
//向上翻,使得翻动次数最少。(-1:不为M的倍数,0
//向上,1向下)
}

/**
主程序类,只有一个主函数,用于以下算法求解
算法说明:
1:我们可以知道如果朝上或者朝下的数目的个数是M的倍数的话,我们就可以马上确定出问题的解。
2:对于这个问题可以用队列来解决。
初始化队列:在队头插入最早的时候,就是用户给我们的状态。
Node start = new Node();

start.up = upNum;
//开始时候向上的硬币的个数
start.down = totalNum - upNum;
//开始时候向下的硬币的个数
start.times = 0;
//在这个状态,它前面没有翻动过硬币
v.add(start);
//插入到队尾
对于每次翻动,我们可以有M+1个翻法,0个翻上,M个翻下;一个翻上,M-1翻下;两个翻上,M-2翻下
...M-2个翻上,2个翻下;M-1个翻上,1个翻下;M个翻上,0个翻下。(说明:对于一个翻下,其他翻上
的情况,会在M-1个翻上的时候发生,还有个需要说明的就是每次都要翻M下,我是把每次朝下的硬币向上
翻,这样就有一种情况,朝下的硬币不够翻,就需要把朝上的硬币朝下翻,这个时候就会会到硬币刚刚好够
向上翻的情况,所以每必要在对这种情况做遍历,这程序中有做说明)。
(1)、我们从初始状态开始,把每它可能的M中翻动方法插入到队列中,并记下它是前面翻过了0下。
(2)、然后在从队头取出一个元素(注意队列不会发生为空的情况,每对一次状态的操作,就会在队列中加人
M个元素,但仅减少一个元素),对取出的元素进行判断,看它是否满足说明1,如果满足就可以有前面翻了几
下+朝上(朝下)/M 确定要多少下可以都朝上或朝下。(当然这个时候可能发现朝上和朝下都为M的倍数,我们
取Min(朝上/M,朝下/M),使得翻动的次数最少),如果不满足说明1,则对它进行M+1次翻转操作(M+1次中
并不是每次都可以,有些情况会重复,在前面说明过),记下它前面翻动了1下,每次的状态都插入到队列中。这
样我们就遍历了翻动一下的所有情况,而每次的翻动都在队列中保存着。

(3)、从队列中取出队首的元素,它是翻动过1次后硬币的状态。对它我们也进行如上的M+1次翻动,这就
会队第3次的硬币所有翻动做了遍历。
(4)、周而复始,对硬币翻动,直到得到解或者内存不足。(说明:如果没有解或者要翻动太多次了,就会发现
内存不足。)

说明:对于这个算法,如果在内存或时间满足的情况下,可以确定最少的操作次数。但是对于这个问题有没有解,
就比较难确定,可以用1:确定最大的移动次数Counter,如果移动的次数大于它,我们认为没有解,2:因
为每次对列中都只减少一个元素,加入M个元素,这样迟早内存会被耗空,可以每次捕获这个例外,当捕获的
时候就认为没有解。由于Java中没有必要捕获内存耗空的例外,所有,我这边就是使用方法2,如果程序程序
运行过程发生例外,就表明这个问题在空间上没有解。
*/
public class UpAndDown
{
static int debug = 0;

public static void main(String[] args)
{
int totalNum = 37;
//总的硬币个数
int upNum = 35;
//朝上的硬币个数
int M = 7;
//每次翻动的硬币个数


if(args.length != 3){
System.out.println("Usage: java UpAndDown totalNum upNum trunoverNum");
System.exit(1);
}
totalNum = Integer.parseInt(args[0]);
upNum = Integer.parseInt(args[1]);
M = Integer.parseInt(args[2]);

if(debug == 1){
System.out.println(totalNum + " " + upNum + " " + M);
}

/*在这里要对参数的合法性做判断,这些细节被我省略了 :)*/
Vector v = new Vector();
//用于保存每次翻动所操作的硬币状态,队头会被删除掉
Vector path = new Vector();
//用于保存每次翻动所操作的硬币状态,只做插入,用于恢复每次操作的硬币状态

//初始化队列,把最先的硬币状态加入。
Node start = new Node();

start.up = upNum;
start.down = totalNum - upNum;
start.times = 0;


v.add(start);

if(debug == 1){
System.out.println("<<Add>> " + start);
}
path.add(new Node(start));

//所有可能的翻动遍历
while(true){
//出队,并且判断是否满足算法说明1,如果满足,找到解,退出遍历
start = (Node)v.remove(0);

if(debug == 1){
System.out.println("<<Remove>> " + start);
}

if(start.isIntegerTimes(M) != -1) {
//System.out.println(start.times + start.isIntegerTimes(M, status));
break;
}

int iUp, iDown;//向上翻iUp个,向下iDown个
//所有可能的翻动都做遍历,发生重复的被忽略过,向上翻动从0到M个。
for(iUp = 0;
iUp <= M;
iUp++){
Node node = new Node();
iDown = M - iUp;
//如果前面那一次的朝下硬币个数比要往上翻的少,就会让朝下都往上翻,朝上有被
//往下翻了,这样就和朝下的硬币=要朝上翻的情况相同了。
if(iUp <= start.down &amp;&amp;
iDown <= start.up){
iDown = M - iUp;

if(iDown <= iUp){//向下翻比向上的少,等同于向上翻iUp-iDown个硬币
node.up = start.up + iUp - iDown;
node.down = start.down - iUp + iDown;

}
else
{//向上翻比向下的少,等同于向下翻iDown-iUp个硬币
if(start.up > (iDown - iUp)){//向下的硬币个要够向上翻。
node.up = start.up - iDown + iUp;
node.down = start.down + iDown - iUp;

}
else
//如果不够向下翻,会于前面的某次等同,所以忽略。
continue;
}
//记下翻动的状态,加入队列中
node.upCounter = iUp;
node.downCounter = iDown;
node.times = start.times + 1;
node.prev = path.indexOf(start);

v.add(node);
path.add(new Node(node));
//拷贝一个信息到Vector中,用于回溯翻动遍历的过程
if(debug == 1){
System.out.println("<<Add>> " + node);
}
}
}

}//end while()

int totalTimes = start.times + start.isIntegerTimes(M);
//总共翻动的次数

if(debug == 1){
Node node;
int i = 0;
Enumeration e = path.elements();

while(e.hasMoreElements()){
node = (Node)e.nextElement();

System.out.println(++i + " Up = " + node.up
+ "do
wn = " + node.down
+ " Times = " + node.times
+ " Turn up = " + node.upCounter);
}
}

//输出要翻动的次数
System.out.println("At least turn over " + totalTimes + " times");


Vector action = new Vector();
String str = "";


//连续向上或者向下翻的次数
if(start.status == 0){
str = "Next,we continue turnning coin up " + start.down / M + " times";
action.add(str);

}
else
if(start.status == 1){
str = "Next, we continue turnning coindo
wn " + start.up / M + " times";
action.add(str);
}
else
{
System.err.println("Have some error.");
System.exit(1);
}

while(start.prev != -1){
action.add(0, start.toString());

start = (Node)path.elementAt(start.prev);

}
action.add(0,start.toString());

//输出每次翻动的状态
System.out.println("-------------------Action:--------------------");
Enumeration e = action.elements();
while(e.hasMoreElements()){
str = (String)(e.nextElement());
System.out.println(str);
}
}
}

/*
输出说明:
如总共6个,五个朝下,翻3个,输出结果如下
3
---------------------------------------------------------
3 Up = 3do
wn =3 Times = 2 upCounter = 2do
wnCounter = 1 Prev = 1
2 Up = 2do
wn =4 Times = 1 upCounter = 2do
wnCounter = 1 Prev = 0
0 Up = 1do
wn =5 Times = 0 upCounter = -1do
wnCounter = -1 Prev = -1

第一行的3表示最少要多少下
下面的行表示每次翻动的状态,从最后一下到第一下。
前面的数字表示它是第几次
Up:这次硬币的朝上个数
Down:朝下个数
Times:在它前面已经翻动了次数,上一次要朝上翻upCounter个,朝下翻downCounter个会出现现在
硬币状态。
Prev:它前一次在path中的索引。
最后一行表示的是初始的状态,Turn up = -1。
后来整数个翻转的就不输出了。
*/
=================================================================
还有一个有意思的问题,a,b,m的解==a,b,a+b-m的解,就是说比如9,9,17
就等于9,9,1,口算得出翻9次,分别为9,9--8,10--7,11--...--1,17--0,18
好了,就写到这。

 
呵呵,确实有个Bug.不是出在求解部分,而是出在print函数.
 
to 叶不归 :
"更无需回溯遍历过程。"
不全对的,你必须要知道最小的路径是哪一条。对于M>a,b时可以用我的互补定理解决。
最麻烦的情况是 a,b,m都很接近的时而且都非常大(比如>10000000)时,
从而导致搜索深度加深从而无法接受。
但这确实可以用动态规划法(记录下每个状态下需要的最少步数)来解决。[:)]
其他的几个算法没时间细看,但我想应该是不能解决大规模问题的。
 
大家一定要看看这个令人吐血的程序!!!!
我相信一定没有比这个算法更快更准确更简练更经典的了。
这个算法源于我另一个同学的以下两个数学定理:
设a上b下翻m
定理一. a,b,m=a,b,a+b-m的解。
定理二. 对于任意整数x,如果m为奇数,则x可以不超过两次就变化为m的倍数。
如果m为偶且x也为偶数,则x可以不超过两次就变化为m的倍数。
由这两个定理,尤其是定理二,就可以很快得找到最优解,且即使a,b,m为任意大
整数的情况也不影响效率!!
证明略。
程序如下:(请大家看清了,只有一个for语句,无嵌套无递归,时间复杂度o(n)
var
a,b,m,Count: Integer;
List: TStringList;
procedure Turn(x, m: Integer);
var
i: Integer;
procedure ListEx(y: Integer);
begin
while y>=0do
begin
List.Add('(' + IntToStr(y) + ',' + IntToStr(a+b-y) + ')');
Dec(y, m);
end;
end;
function Min(m1, m2: Integer): Integer;
begin
Result := m1;
if m1>m2 then
Result := m2;
end;
begin
for i := Min(x, m)do
wnto 0do
if (x-i+m-i) mod m = 0 then
begin
List.Add('(' + IntToStr(a) + ',' + IntToStr(b) + ')');
ListEx(Abs(x-i+m-i));
Exit;
end;
i := 2*m;
while i>=0do
begin
if (x+i) mod m = 0 then
begin
List.Add('(' + IntToStr(a) + ',' + IntToStr(b) + ')');
if x>m then
List.Add('(' + IntToStr((x-m) div 2) + ',' + IntToStr(a+b-((x-m)div 2)) + ')')
else
List.Add('(' + IntToStr((x+m) div 2) + ',' + IntToStr(a+b-((x+m)div 2)) + ')');
ListEx(x+i);
end;
Dec(i, 2);
end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
s: string;
begin
a := StrToInt(Edit1.Text);
b := StrToInt(Edit2.Text);
m := StrToInt(Edit3.Text);
Count := 0;
if (m mod 2=0) and (a mod 2=1) and (b mod 2=1) or (a+b<m) or (a+b=m) and (a*b<>0) then
begin
Memo1.Lines.Add('无解');
Exit;
end;
List := TStringList.Create;
if m > (a+b) div 2 then
m := a+b-m;
if not ((m mod 2=0) and (a mod 2=1)) then
Turn(a, m);
Count := List.Count;
s := StringReplace(List.Text, #13#10, '-', [rfReplaceAll]);
List.Clear;
if not ((m mod 2=0) and (b mod 2=1)) then
Turn(b, m);
if ((Count<List.Count) or (List.Count=0)) and (Count>0) then
Memo1.Lines.Add(s)
else

Memo1.Lines.Add(StringReplace(List.Text, #13#10, '-', [rfReplaceAll]));
List.Free;
end;
=======================================
唉,数学真是个奇妙的世界!
其实这个也是很简单的数学规律,四年数学真是白学了:(
 
好算法
 
我也来UP
LEE大侠是在上海工作吗?
 
刚才看了一下回帖,原来天真已经说出来最优算法,只是不够详细
1,取两数较小者,大部分可以,但不严密,例如16,17,7等等
2,只要把a减小到2m或2m以内,就行了,这样省了一步,就对了
这里就要求m<(a+b)/2
其实就是叶不归所说的定理一,二。嘿嘿!
 
to 叶不归:
您说的定理二我无法证明,我可以证明在 a>2*m而且 b>2*m的时候该定理成立,而且
应该是 “对于任意整数x,如果m为奇数,则x可以不超过一次就变化为m的倍数...”
如果您可以将详细证明写出来,我愿意出600分表示感谢。不知肯否?[:)]
btw:
您说的定理一是不严密的,应该说他们是互补解------我的互补原理就是这样。
您的那个程序还是有些小bug的。
 
DarwinZhang大侠:
>>应该是 “对于任意整数x,如果m为奇数,则x可以不超过一次就变化为m的倍数...”
这个我不同意,因为m为偶,x为偶的情况也是:则x可以不超过一次就变化为m的倍数...”
证明我要想一下。
有什么bug请说明啊,我是想把这个解法做完善的。
 

Similar threads

S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
2K
SUNSTONE的Delphi笔记
S
D
回复
0
查看
2K
DelphiTeacher的专栏
D
D
回复
0
查看
2K
DelphiTeacher的专栏
D
D
回复
0
查看
1K
DelphiTeacher的专栏
D
后退
顶部