很久很久之前(汗...),
kaikai给了我一个flash游戏玩(放在这
下载了),大概的意思是
5个人在分别再17楼,26楼,20楼, 19楼,31楼等着下电梯,他们下电梯的位置都是在21~25层楼之间(包含)但是规定只能两个人同时上或者同时下电梯,并且上电梯只能一次上升8个单位,下降只能下降13个单位,其中楼层高度为49层楼,现在让你通过操作这么5个人,使得他们能够顺利的到达指定楼层。
当时kaikai貌似是用手算然后得出结果8的,我当时第一印象是写段程序解决,后来时间不够就丢在一边了,最近整理硬盘的时候无意中发现了,就着手把这个做完了。
把这个游戏中中的模型抽象出来为:
有5个数字, 17, 26, 20, 19, 31, 现在要求同时操作其中的两个数字,使他们同时+8,或者同时-13,使得所有的数字最终都处在[21,25]之间,注意中间结果必须在[1,49]之间,求最少需要操作的次数?

下面的分析是结合Felica, 怎么我也,kaikai,gardon, dj都诸位大牛基础上分析的。
首先分析一下可行性,由于8和13互质,根据
(裴蜀恒等式)如果两个数a,b的最大公约数是d,那么存在两个整数x与y,使得等式ax+by=d成立。
则存在8x + 13y = 1,当然也就存在8x - 13y = [1, 49], 因此处在每一层上的人都有可能到达目标层。
那么怎么处理这个问题呢,一个比较直接的想法:是将这5个人所处的位置作为一个状态,然后进行bfs,将每一个状态保存,最终得到结果。当我写完程序之后发现这个无比缓慢,因为一个状态会派生出C(5, 2) * 2=20个状态,结果会很慢,实验结果等待了5分钟也没有结果。那么该怎么处理呢?剪枝,对了!
dj师兄告诉我可以用“胡乱剪”,就是将一些看似不可能的状态直接给去掉,不用扩展,这样带来的后果是结果可能不正确,牺牲正确性带来效率,有点“Monte Carlo”算法味道。Felica推荐了hash。这里我用了后者,由于所处的层数只是在1~49之间,而49 < 63 = 2^6 - 1,因此将每一个状态里面5个人的层数分配给6位组合成一个int的数字就能避免了hash的冲突,也就带来了效率。
下面是实现的C++代码:


#include <iostream>
#include <queue>

using namespace std;

const int PEOPLE_NUM = 5; //总人数
const int TOP_LIFT = 49; //楼梯最高层
const int BOTTOM_LIFT = 1; //楼梯最低层
const int TARGET_LOW = 21; //目标层较低层
const int TARGET_HIGH = 25; //目标层较高层
const int MOVE_UP = 8; //向上移动的单位
const int MOVE_DOWN = 13; //向下移动的单位

const int FLOOR[PEOPLE_NUM] =
{17, 26, 20, 19, 31};

bool Hash[0x2fffffff]; //防止状态重复的Hash解决方法

struct State


{
int p[PEOPLE_NUM]; //人所处的层数
int up[PEOPLE_NUM]; //人已经上了多少层
int down[PEOPLE_NUM]; //人已经下了多少层
int no;//移动的次数 //一个操作了多少次,这个为了方便,其实最后上下楼梯之和加起来除以2便是

//得到状态码,最为hash的key
int GetHashCode()

{
int result = 0;
result |= p[0];
result |= p[1] << 6;
result |= p[2] << 12;
result |= p[3] << 18;
result |= p[4] << 24;

return result;
}
};

//判断状态是否满足题意,即每个人都处在要下楼梯之间
bool IsSatisfy(State s)


{
for(int i = 0; i < PEOPLE_NUM; i++)

{
if( !(s.p[i] >= TARGET_LOW && s.p[i] <= TARGET_HIGH) )
return false;
}

return true;
}

//打印最终结果
void Output(State s)


{
for(int i = 0; i < PEOPLE_NUM; i++)

{
cout << i + 1 << "个人升" << s.up[i] << "次,降" << s.down[i] << "次 最终到达" << s.p[i] << "层" << endl;
}
}

int main()


{
//初始化状态
State start;
start.no = 0;
for( int i = 0; i < PEOPLE_NUM; i++ )

{
start.p[i] = FLOOR[i];
start.up[i] = start.down[i] = 0;
}

Hash[start.GetHashCode()] = true;

//用队列实现bfs(废话
-_-)
queue<State> q;
q.push(start);

while(!q.empty())

{
State f = q.front();
q.pop();

if( IsSatisfy( f ) )

{
Output(f);
cout << f.no << endl;
break;
}
//将状态进行扩展
for(int i = 0; i < PEOPLE_NUM; i++)

{
for(int j = i + 1; j < PEOPLE_NUM; j++)

{
if( f.p[i] + MOVE_UP <= TOP_LIFT
&& f.p[j] + MOVE_UP <= TOP_LIFT )

{
State s = f;
s.p[i] += MOVE_UP;
s.p[j] += MOVE_UP;
s.no = f.no + 1;
s.up[i]++;
s.up[j]++;
if(!Hash[s.GetHashCode()])

{
q.push(s);
Hash[s.GetHashCode()] = true;
}
}
if(f.p[i] - MOVE_DOWN >= BOTTOM_LIFT
&& f.p[j] - MOVE_DOWN >= BOTTOM_LIFT )

{
State s = f;
s.p[i] -= MOVE_DOWN;
s.p[j] -= MOVE_DOWN;
s.no = f.no + 1;
s.down[i]++;
s.down[j]++;
if(!Hash[s.GetHashCode()])

{
q.push(s);
Hash[s.GetHashCode()] = true;
}
}
}
}

}

return 0;
}

下面是运行结果:
1个人升1次,降0次 最终到达25层
2个人升3次,降2次 最终到达24层
3个人升2次,降1次 最终到达23层
4个人升2次,降1次 最终到达22层
5个人升2次,降2次 最终到达21层
8
嗯, 现在看来工作已经差不多了,可是您可能会发现上面的算法是多么的低效,保留了很多无用的状态,仅仅是利用这些状态去扩展.下面我们变化思路,先求得各个人到达目标层的单个状态(SingleState),然后再不断的组合次优解,进而判断当前的整合状态是否能够满足"抵消"(这里的抵消指的是同时以不同的两个电梯升或者降使得最终没有剩余.)这个数学模型可以理解为有若干不同容量的箱子,里面装满了小球,现在有两个人,每次从不同的盒子里面拿球,判断最终是否有小球剩余.这个根据Gardon大牛的指示,可以每次取最大和次大的中的每一个球,
得到的结论是当总的球数为偶数并且最大球的数目<= 总数的1/2就可以了.(具体证明还不会...汗).
我下面就是使用了5个队列不断的去求得每个电梯的最优解,然后进行组合,这样就相比上面高效多了,
下面是具体的实现代码:


#include <iostream>
#include <queue>

using namespace std;

const int PEOPLE_NUM = 5; //总人数
const int TOP_LIFT = 49; //楼梯最高层
const int BOTTOM_LIFT = 1; //楼梯最低层
const int TARGET_LOW = 21; //目标层较低层
const int TARGET_HIGH = 25; //目标层较高层
const int MOVE_UP = 8; //向上移动的单位
const int MOVE_DOWN = 13; //向下移动的单位

const int FLOOR[PEOPLE_NUM] =
{17, 26, 20, 19, 31};

bool hash[PEOPLE_NUM][0xfffffff]; //防止重复单个状态重复

struct SingleState


{
int floor;//所处的层数
int up;//上升的次数
int down;//下降的次数

//状态码,用来作为Hash的key
int GetHashCode()

{
return (up << 14) | down;
}
};

struct State


{
//组合单个状态
SingleState ss[PEOPLE_NUM];
//用于优先级队列的元素比较,这里当然以上下的总数比较了(注意STL里面默认的是最大堆),kaikai提醒,不要忘了常引用,
//否则太低效了
bool operator < (const State& s) const

{
int sum1 = 0, sum2 = 0;
for( int i = 0; i < PEOPLE_NUM; i++ )

{
sum1 += s.ss[i].up + s.ss[i].down;
sum2 += this->ss[i].up + this->ss[i].down;
}
return sum1 < sum2;
}
};

//单个目标状态队列
queue<SingleState> p[PEOPLE_NUM];

//初始化单个队列
void InitializeQueue()


{
SingleState s[PEOPLE_NUM];
for( int i = 0; i < PEOPLE_NUM; i++)

{
s[i].floor = FLOOR[i];
s[i].up = s[i].down = 0;
hash[i][s[i].GetHashCode()] = true;
p[i].push(s[i]);
}
}

//判断单个状态是否满足
bool Satisfy(SingleState s)


{
return s.floor >= TARGET_LOW && s.floor <= TARGET_HIGH;
}

//判断整体状态是否满足,利用1/2的结论
bool Satisfy(SingleState s[])


{
int sum1 = 0, sum2 = 0;
int max1 = -1, max2 = -1;
bool succeed = true;
for( int i = 0; i < PEOPLE_NUM; i++)

{
sum1 += s[i].up;
sum2 += s[i].down;
if(max1 < s[i].up)
max1 = s[i].up;
if(max2 < s[i].down)
max2 = s[i].down;
}
if( ( sum1 & 1 ) || ( max1 > sum1 / 2 ) )
return false;
if( ( sum2 & 1 ) || ( max2 > sum2 / 2 ) )
return false;

return true;
}

//利用bfs获取单个状态的“目标”状态
SingleState GetSuper(int i)


{
while(!p[i].empty())

{
SingleState s = p[i].front();
p[i].pop();
if(s.floor + MOVE_UP <= TOP_LIFT)

{
SingleState ss = s;
ss.up++;
ss.floor += MOVE_UP;
if(!hash[i][ss.GetHashCode()])

{
p[i].push(ss);
}
}
if(s.floor - MOVE_DOWN >= BOTTOM_LIFT)

{
SingleState ss = s;
ss.down++;
ss.floor -= MOVE_DOWN;
if(!hash[i][ss.GetHashCode()])

{
p[i].push(ss);
}
}
if(Satisfy(s))
return s;
}
}

//打印结果
void Output(SingleState s[])


{
for( int i = 0; i < PEOPLE_NUM; i++ )

{
cout << "第" << i + 1 << "个人升" << s[i].up << "次,降" << s[i].down << "次 最终到达" << s[i].floor << "层" << endl;
}
}

int main()


{
int i;

SingleState s[PEOPLE_NUM];//单个状态
State gloS;//整体装体
InitializeQueue();

for(i = 0; i < PEOPLE_NUM; i++)

{
s[i] = GetSuper(i);
gloS.ss[i] = s[i];
}

priority_queue<State> pq;//优先级队列,以上下楼层总数作为关键码
pq.push(gloS);

while(!pq.empty())

{
State tmpS = pq.top();
pq.pop();
if(Satisfy(tmpS.ss))

{
Output(tmpS.ss);
break