这个构造函数不指定任何初始条件,包括当前状态。可以在需要的时候使用一系列的set成员函数定义。
接下来定义一个“全功能”的构造函数:
WalkThrough(const State& s, StateJumper j = 0, Matcher m = 0)
: overFlag(false), curState(s), Jumper(j), IsMatch(m) { }
除了overFlag外,所有的属性都可以在这个构造函数中设定(当然,它允许缺省值)。由于没有进行任何穷举操作,将overFlag强制为false是合理的。
对于拷贝构造函数,由于我们这里没有涉及内存分配,没有“深拷贝”的需求,因此不作定义,使用默认的位拷贝可以有不错的效率。类似地,析构函数也没有什么事务需要处理,不过考虑到这个WalkThrough可能用于继承,且有可能出现delete基类指针来删除派生对象的情况,便定义一个空的虚析构函数,以免引起错误:
virtual ~WalkThrough() { }
最后,我们来实现唯一的一个非内联函数:getNextFilter(),在给出实现之前顺便给出完整的 WalkThrough的定义:
template <class State>
class WalkThrough
{
public:
typedef bool (*StateJumper)(State&);
typedef bool (*Matcher)(const State&);
WalkThrough(): overFlag(false), Jumper(0), IsMatch(0) { }
WalkThrough(const State& s, StateJumper j = 0, Matcher m = 0)
: overFlag(false), curState(s), Jumper(j), IsMatch(m) { }
virtual ~WalkThrough() { }
void setJumper(const StateJumper j) { Jumper = j; }
void setMatcher(const Matcher m) { IsMatch = m; }
void setState(const State& s) { curState = s; }
State getState() const { return curState; }
bool getNextFilter();
bool isOver() const { return overFlag; }
private:
State curState;
bool overFlag;
StateJumper Jumper;
Matcher IsMatch;
};
template <class State>
bool WalkThrough<State>::getNextFilter()
{
if (overFlag) // 若已完成遍历,则直接返回
return false;
if (!Jumper || !IsMatch) // 若用户未定义Jumper或IsMatch函数,则返回
{
overFlag = true; // 这里将没有定义Jumper或IsMatch的穷举视为遍历完成
return false; // 不过,如果你认为两者绝不能等同,也可以抛出异常
}
while (!(overFlag = !Jumper(curState)) && !IsMatch(curState))
; // 获取下一状态,直到找到可行状态或者遍历完成
if (overFlag) // 根据遍历完成情况决定返回值
return false;
else
return true;
}
呼……小功告成,总算可以小松一口气。不过如果到此就关闭计算机,就像直播球赛进行到高潮时突然停电一样,太令人不爽。——弄了这么久,总该给个example试一试,来点成就感吧?
没问题。苹果问题太简单,于是我们选择的是一个传说中的“和是50”超级难题,什么是“和是50”超级难题?请听好了:所谓“和是50”超级难题,就是在0到99的整数组成的一个加法算式中,找出和是50的算式,考虑加数顺序,像0+50啊,1+49啊,……,49+1啊,50+0啊……(喂,我知道你可能想杀人,但请不要让与之匹配的眼光朝着我……)对于这样一个超级难题,我们经过研究,决定采用……穷举法来实现(哪来的这么多西红柿?)。
首先我们研究这个问题的状态是什么——两个加数,不是么,它们是有顺序的,并且可以取0到99的整数。于是乎,我们定义状态类如下(注:STL中有类似的东西,但作为完整的示例,我们亲自手工打造):
class Pair
{
public:
Pair(int mx = 0, int my = 0): x(mx), y(my) { }
int x, y;
};
Pair虽然属于世界上最深奥的class之一,然而凭各位的智慧,我就不用多解释了。接着我们来看一下跳转函数的实现——当然,它应当符合WalkThrough中定义的StateJumper类型:
bool counter(Pair& pair)
{
if (pair.y < 99)
++pair.y;
else
{
if (pair.x < 99)
{
++pair.x;
pair.y = 0;
}
else
return false;
}
return true;
}
counter的作用是试图将pair.y增1,但如果pair.y已经到达上限99,则将pair.x加1,同时pair.y置0继续下一轮;但如果pair.x也到了99,那么,噢,说明遍历结束了。——嗯,这样的确是一种有效的遍历方式,对吧。它实际上类似于下面的二重循环:
for (; pair.x <= 99; ++pair.x)
for (pair.y = 0; pair.y <= 99; ++pair.y)
...
只不过循环要求在循环体内解决一切问题,而我们“解决一切问题”的重任担在了另一个函数里,counter不能越权,因而不能这样直接使用循环。
接下来是判断是否为有效状态的函数,它最简单了:
bool match(const Pair& pair)
{
return (pair.x + pair.y) == 50;
}
万事俱备,可以写main:
int main()
{
WalkThrough<Pair> sf50(Pair(0, -1), &counter, &match);
while (sf50.getNextFilter())
cout << sf50.getState().x << " + "
<< sf50.getState().y << " = 50" << endl;
return 0;
}
请一定要注意,getNextFilter()中有一个"Next",也就是说,如果把状态全集中的第一个状态作为构造的参数,——本题中就是Pair(0, 0)——是可行状态的话,getNextFilter()将不会判断到,它直接判断“下一个”状态,就是Pair(0, 1)去了。为了让它能够完整地判断,我们不能让Pair(0, 0)作为第一个状态,而是作为首次运行getNextFilter()后到达的状态。于是,在构造初始状态时我们就要用“前”一个状态Pair(0, -1)作为构造参数(虽然它根本不在本题的状态全集中),以使得getNextFilter()从Pair(0, 0)开始判断。
嗯,好,在运行之前不要忘了#include <iostream>,不要忘了打开std名字空间(using namespace std;),不要忘了粘上/包含上前面的WalkThrough的完整定义……编译,运行,好,这个超级难题被我们解决了。我的编译平台是Redhat Linux 9, GNU C++ 3.2.2.
最后我们分析小结一下经过封装之后的穷举算法。首先看效率——是啦,我知道这个“和是50”超级难题所用的算法很糟糕——我是指在相同算法的前提下,使用我们封装好的穷举类进行实现时的效率损失:初始化以及一系列set/get函数基本不影响效率,主开销源自于getNextFilter()本身的调用及getNextFilter对两个用户提供函数的调用所产生的额外开销。试想如果用相同的算法,但仅用两层for来做穷举,则少了上万次的函数入栈、清栈操作。穷举法本身就不是一个高效的搜索思想,所以对内层代码的效率变化是最敏感的,这一点是我们所封装的穷举类的最大缺点。
不过总算还是有一点好处,比如,封装之后,就将设计穷举算法分解成了相对独立的设计状态类、状态跳转函数、状态判断函数三个部分,三者的耦合度相对降低。此外,这个设计的过程多少让我们又多了解了一点点基于对象编程的抽象能力,嗯,等等。噢……或许这是一个学术价值大于实用价值的设计吧,呵呵。








