趣学算法学习笔记2

问题 3-6 最好的农场

问题描述

背景

在一场反侵略战争中,农夫 William 团结了全国的农民帮助国王抗击侵略者。战争胜利了,国王决定奖赏给农夫 William 一个大农场。

问题

国王将其国土划分成 1×1 见方的 A×B 个格子,每个格子用一对整数作为标识。如图 3-6 所示。不过,并非所有的格子都可奖赏给 William,其中一些已经奖赏给别人,另一些在战争中被摧毁。国王仅列出那些可以作为奖品的格子供 William 选择。当然,William 也不能将这些格子全部用来建造他的农场,他只能选择连接区域来建立农场。所谓连接

在这里插入图片描述

区域定义如下:

1 一块连接区域由若干个 1×1 的格子组成。
2 从区域中任何一个 1×1 格子可不经过任何不属于本区域的格子而进入本区域中的另一个格子。
3 在一个格子中,可从东、南、西、北四个方向进入本区域的相邻格子。此外,每一个格子都有其价值。William 要选择一块价值最大的连续区域来建立他的农场。也就是说,William 应选择能构成连续区域的那些格子,并且使得这些格子的价值之和最大。在本问题中,你的任务就是要找出此最大价值。

输入

输入含有若干个测试案例,每个测试案例的第一行含有一个整数 N,表示可作为奖励的格子数。而后跟着的 N 行每行包含这 N 个格子之一的信息。每行含有三个整数 x,y,v,整数之间用空格隔开。其中,(x, y)表示格子的位置,而 v 表示该格子的价值。所有的 x 和 y都是 16 比特位的整数,v 是介于 0~10000 的正整数。以 0 开头的测试案例是输入中最后的案例,无须输出。

输出
对每一个测试案例,输出一行,其中包含作为答案的整数,即 William 所获奖励的连续区域的最大价值。

输入样例

1
0 0 1
6
0 1 1
0 0 1
1 0 1
2 2 2
2 1 2
2 -1 1
0

输出样例

1
4

解题思路
(1)数据的输入与输出
按输入文件的格式,依次从输入文件中读取各案例。每个案例的第一行仅含一个表示国王可以奖给 William 的方格个数 N。接着的 N 行每行包含描述一个方格的三个整数 x, y, v。将这些数据组织成数组 cells。对 cells 计算 William 可以选取的价值最大的连续地块的价值。将算得的结果作为一行写入输出文件。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 N
while N>0
	do 创建数组 cells←∅, values←∅
		for i←1 to N
			do 从 inputdata 中读取 x, y, v
				APPEND(cells, (x, y))
				APPEND(values, v)
		result←THE-BEST-FARM(cells, values)
		将 result 作为一行写入 outputdata
		从 inputdata 中读取 N
关闭 inputdata
关闭 outpudata

其中,第 10 行调用计算连续地块最大价值的过程 THE-BEST-FARM(cells)是解决一个案例的关键。

(2)处理一个案例的算法过程对于一个案例的数据 cells[1…n]和 values[1…n],为计算方格中能组成连续地块的格子价值总和,为每个格子 cells[i]维护一个属性 v i s i t e d [ i ] ( 1 ≤ i ≤ n ) visited[i](1≤i≤n) visited[i](1in),初始化为 false,用来指示是否处理过。设置一个队列 Q(初始化为∅)。还要设置一个跟踪地块最大价值的变量 max(初始化为−∞)。扫描整个数组 cells,一旦当前格子 cells[i]未曾访问过(visited[i]=false),这意味着找到一块新的连续地块的起点。设置地块价值 value(初始化为 0),将 visited[i]置为 true,并将 i加入 Q。只要 Q 非空,将 Q 的队首出队记为 k,将 values[k]累加到 value,在 cells 中查找与cells[k]相邻的未曾访问过的格子 cells[j],将将 visited[j]置为 true,并将 j 加入 Q。循环往复,直至 Q 为空。这意味着一块连续地块搜索完毕,其价值记录在 value 中。将 value 与 max 加以比较,若 max<value 则 max 跟踪 value。当对 cells 的扫描结束,max 即为所求。

THE-BEST-FARM(cells, values)
n←length[cells]
max←-∞
创建 visited[1..n]{fals, fals, ..., fals}
	for i←1 to n
		do if visited[i]=false
			then value←0
				visited[i]true
				Q←∅, PUSH(Q, i)
				while Q≠∅
					do k←POP(Q)
					value←value+values[k]
					for each 与 cells[k]相邻的 cells[j]
						do if visited[j]=false
							then PUSH(Q, j)
								visited[j]true
					if max<value
						then max←value
return max			

算法 3-9 解决“最好的农场”问题一个案例的算法过程

算法中第 4~17 行的 for 循环重复 n 次。由于每个格子在第 9~17 行的内嵌 while 循环中有一次且只有一次进入队列 Q,所以第 10~17 行的操作总共被执行Θ(n)。注意,第 12~15 行虽然表示成 for 循环,其实最多重复 4 次,因为与 cells[k]相邻的格子最多只有四个(上、下、左、右) 。每次需要在 cells 中进行查找,耗时Θ(n)。所以该 for 循环的耗时为 4Θ(n),在渐近表达式中等价于Θ(n)。于是算法 3-9 的运行时间为Θ(n2)。解决本问题的算法的 C++实现代码存储于文件夹 laboratory/The Best Farm 中,读者可打开文件 The Best Farm.cpp 研读,并试运行之。C++代码的解析请阅读第 9 章 9.4.1 节中程序9-44 的说明。

#include <iostream>
#include <fstream>
#include <queue>
#include <vector>
#include <climits>
#include <string>
#include <algorithm>
using namespace std;
int theBestFarm(vector<pair<int,int>> &cells,vector<int>&values)
{
	int n=cells.size();
	int max=INT_MIN;
	vector<bool> visited(n,false);
	for(int i=0;i<n;i++)
	{
		if(!visited[i])
		{
			int value=0;
			visited[i] = true;
			queue<int> Q;
			Q.push(i);
			while(!Q.empty())
			{
				int k=Q.front();Q.pop();
				value+=values[k];
				int x=cells[k].first,y=cells[k].second;
				vector<pair<int,int>>::iterator p;
				p=std::find(cells.begin(),cells.end(),pair<int,int>(x-1,y));
				if(p!=cells.end())
				{
					int j=distance(cells.begin(),p);
					if(!visited[j])
					{
						Q.push(j);
						visited[j]=true;
					}
				}
				p=std::find(cells.begin(),cells.end(),pair<int,int>(x+1,y));
				if(p!=cells.end())
				{
					int j=distance(cells.begin(),p);
					if(!visited[j])
					{
						Q.push(j);
						visited[j]=true;
					}
				}
				p=std::find(cells.begin(),cells.end(),pair<int,int>(x,y-1));
				if(p!=cells.end())
				{
					int j=distance(cells.begin(),p);
					if(!visited[j])
					{
						Q.push(j);
						visited[j]=true;
					}
				}
				p=find(cells.begin(),cells.end(),pair<int,int>(x,y+1));
				if(p!=cells.end())
				{
					int j=distance(cells.begin(),p);
					if(!visited[i])
					{
						Q.push(j);
						visited[j]=true;
					}
				}
			}
			if(max<value)
				max=value;
		}
	}
	return max;
}


int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	int N;
	inputdata>>N;
	while(N>0)
	{
		vector<pair<int,int>>cells;
		vector<int>values;
		for(int i=0;i<N;i++)
		{
			int x,y,v;
			inputdata>>x>>y>>v;
			cells.push_back(pair<int,int>(x,y));
			values.push_back(v);
		}
		int result=theBestFarm(cells,values);
		outputdata<<result<<endl;
		cout<<result<<endl;
		inputdata>>N;
	}
	inputdata.close();
	outputdata.close();
	return 0;
}

基于二叉堆的优先队列及其应用

所谓“优先队列”指的是队列中的元素均有一个优先级,出队按优先级的高低决定先后顺序,每次都是优先级最高的出队。实现优先队列的方法很多,最直接的方法是用数组保存队列中的元素,每当加入一个元素后就对数组按元素优先级进行一次排序,则首(尾)元素必为优先级最高者,出队操作就可方便地执行,每次加入新的元素将耗时Θ(nlgn)。还可以利用基于平衡搜索树的集合实现优先队列。在一棵平衡搜索树中插入元素,耗时为Θ(lgn),且插入后仍为一棵平衡搜索树,按中序遍历的首(尾)元素即为优先级最高者,故出队操作的时间效率也是Θ(lgn)。优先队列经典的实现方法是借助一种称为“二叉堆”的数据结构存储元素。

所谓二叉堆是一棵用数组 heap[1…n]表示的二叉树:存储在 heap[i]处的节点,其左孩子为 heap[2i],右孩子为 heap[2i+1], i=1, 2, …, n/2。并且满足条件:任一节点的值均不小(大)于其孩子的值,如图 3-7 所示。这样,heap[1]必为值最大(小)者。显然,含有 n 个元素的二叉堆中,内点(有孩子的节点)和叶子(没有孩子的节点)各占一半。内点分布于 heap[1…n/2],而叶子分布于 heap[n/2+1…n]。由于存放在数组中的二叉堆必为一棵平衡树,故含有 n 个元素的二叉堆的树高 h 必为Θ(lgn)。

图 3-8 所示的是在最大堆中插入元素的操作。图中(a)表示将值为 15 的元素添加到数组的末尾(即作为树中的最后一片叶子)。由于该节点的值大于其父亲的值 7,两者交换得到(b)。在(b)中,值为 15 的节点仍然比其父亲的值 14 大,两者交换得到最大堆©。节点 15 从叶子逐层上升到合适位置(使得所有节点与其孩子符合堆性质)的操作称为“上升”。由于其中的交换操作最多到达根为止,故所需运行时间为树高Θ (lgn)。

在这里插入图片描述

在这里插入图片描述

图 3-9 所示的是在一个最大堆中删除优先级最高元素的操作。图中(a)表示将树根移除,并将堆中最后一片叶子(其值为 1)移到树根。此时,树根与其两个孩子不满足最大堆性质(父亲的值小于孩子的值),将根与孩子中的较大者交换,得到(b)。在(b)中,值为1 的节点与其孩子(值为 8 和 7)仍然不满足最大堆性质,与其中较大者交换得到©。对©继续同样的操作,得到最大堆(d)。节点从根开始逐层下移到合适位置(使得所有节点与其孩子满足堆性质)的操作称为“下筛” 。和上升操作相仿,下筛操作的运行时间也是Θ (lgn)。因此,基于二叉堆的优先队列的入队和出队操作的运行时间都是Θ (lgn)。

对二叉堆中元素 heap[i]的上升、下筛操作的伪代码过程如算法 3-10 所示。

SIFT-DOWN(heap, i)
	n←length[heap]
	if i>n/2
		then return
 j←A[i], A[2i], A[2i+1]最大者下标
 while i>n/2 and A[i]≠A[j]
 	do exchange A[i] ↔A[j]
		i←j
		j←A[i], A[2i], A[2i+1]最大者下标
LIFT-UP(heap, i)
if i≤1
	then return
while i > 1 and A[i/2] < A[i]
do exchange A[i] ↔ A[i/2]
	i ←i/2

在这里插入图片描述

利用算法 3-10 中的 LIFT-UP 和 SIFT-DOWN 过程不但可以对基于二叉堆的优先队列进行元素的入队和出队操作,还可以在队列中元素的优先级发生变化后恢复堆的性质。基于二叉堆的优先队列的 C++语言实现代码存储为文件 laboratory/utility/PriorityQueue.h,读者可打开此文件研读。C++代码的解析请阅读第 9 章 9.3.2 节中程序 9-31~程序 9-34 的说明。

问题 3-7 David 购物

David 到成都来参加 ACM-ICPC。成都是个美丽的城市, David想给他的朋友买一点礼物。David 的衣袋实在太小,只能装下 M 件礼物。考虑到礼物的多样性,David 不会买多件相同的礼物。David 想挑选一些能表现出典型成都风味的礼物。David 沿着购物街从北向南访问 N 家店铺,每家店铺只有一种礼物出售。David 记性不好,他不记得有多少家店铺出售礼物 K。于是,他将购买的礼物记上标记 L,表示有多少个商铺在出售礼物 K。David 认为标记 L 的值越小越好(David 喜欢不常见的东西) 。当 David 来到一个出售礼物 K 的商店时,他要对付如下三种可能的情形之一。1 若衣袋中还没有礼物 K,且衣袋尚有空间,则毫不犹豫地买下它。在将礼物放入衣袋中之前,David 在其上面记下“1”,表示第一次看到该礼物的出售。2 若礼物 K 已经在他的衣袋中,David 将把记在礼物上的标记 L 改为 L+1,表示已有L+1 家商店出售该礼物。3 若衣袋中没有礼物 K,且衣袋已满,David 会认为从没哪家店铺卖过礼物 K(因为他不记得是否遇到过礼物 K),于是他会放弃衣袋中的一件礼物,为礼物 K 腾出空间,并买下礼物 K。他会按下列原则决定放弃包中哪一件礼物:他选择标志 L 最大的礼物。若有多个礼物具有相同的最大数 L,他将放弃最先放入衣袋中的那件礼物。在放弃了该件礼物后,将礼物 K 的标记记为“1”,然后放入包中。写一个程序,记录下 David 所放弃的礼物次数。例如:David 的衣袋只能放入两件礼物。购物街上有 5 个店铺,每个店铺仅出售一种礼物。它们出售礼物的编号序列为 1,2,1,3,1。在第一个店铺里,衣袋是空的,所以他买下礼物 1,并在其上记下“1”,放入包中。当 David 来到第二个店铺时,衣袋还可放入一个礼物。于是他买下礼物 2,同样在其上记下“1”,放入包中。当他来到第三家店铺时,由于包中已有礼物 1,故他将包中礼物 1 的标记改为“2”。访问第四个店铺时,衣袋已满,但其中并没有礼物 3。于是他要放弃包中的一件礼物,来装下要买的礼物 3。包中礼物 1 的 L 标志为“2” ,礼物 2 的 L 标志为“1” ,故他将放弃礼物 1。在第五家店铺,衣袋已满,礼物 1 没在包中。他需要放弃包中一件礼物,为礼物 1 腾出空间。包中的两件礼物是礼物 2 和礼物 3,它们的 L 标志都是“1”。但礼物 2 先于礼物 3 放入包中,故放弃礼物 2。买下礼物 1,在其上记下“1”,放入包中。逛街结束时,David 的衣袋中有两件礼物,分别为礼物 1 和礼物 3,它们的 L 标志都是“1”。放弃的礼物次数为 2。

输入

输入包含若干个测试案例。每个案例包含两行数据。案例的第一行用两个正整数 M 和 N(M ≤ 50 000 及 N ≤100 000)分别表示衣袋能装下的礼物数及购物街上的商家数。第二行含有 N 个正整数 Ki (Ki <220, i=1, 2, …, N),表示第 i家商店出售的礼物种类编号。M=0 且 N=0 是文件的结束标志,程序无需对其做任何处理。

输出

对每一个测试案例按输出样例的格式输出一个整数。

输入样例

3 5
1 2 3 2 4
2 4
1 2 2 1
2 6
1 2 2 1 1024 1
2 1
1048575
6 16
10 1 2 3 4 5 6 1 2 3 6 5 4 10 1 6
0 0

输出样例

Case 1: 1
Case 2: 0
Case 3: 2
Case 4: 0
Case 5: 3

解题思路

(1)数据的输入与输出

根据输入文件的格式,依次读取每个测试案例的数据。从案例的第一行读取表示衣袋载荷及商铺数的 M 和 N。接下来读取表示各商家所售物品编号的 N 个整数,组织成数组 shops。对 M 和 shops,计算 David 在购物过程中因袋满而放弃礼物的次数,将计算结果作为一行写入输出文件。循环往复,直至读到的 M=0 且 N=0。

打开输入文件 inputdata
创建输出文件 outputdata
num←0
从 inputdata 中读取 M 和 N
while M>0 or N>0
	do 创建数组 shops←∅
		num←num+1
		for i←1 to N
			do 从 inputdata 中读取 K
				APPEND(shops, K)
	result← DAVID-SHOPPING(M, shops)"Case num: result"作为一行写入 outputdata
	从 inputdata 中读取 M 和 N
关闭 inputdata
关闭 outpudata

其中,第 11 行调用计算 David 在购物过程中放弃物品次数的过程 DAVID-SHOPPING(M,shops)是解决一个案例的关键

(2)处理一个案例的算法过程

对一个案例数据 M 和 shops,用一个优先队列来模拟 David 的购物过程。将 David 衣袋设置为一个优先队列 pocket,放入 pocket 中的礼物<K, L>的优先级为标记 L 的值,当有若干个礼物有相同的标记 L 值时,放入袋 pocket 中的时间更早的优先级更高。这样,每当 David看到新礼物而衣袋已满时,就从袋中取出优先级最高者放弃掉,跟踪放弃礼物的次数 count。将此想法写成伪代码过程如下

DAVID-SHOPPING(M, shops) M 为衣袋容量,shops 为每家商铺所售礼物编号数组
pocket←∅, n←length[shops]
discard←0 count 为放弃礼物次数
for i←1 to n 依次进入每一家店铺
	do gift←<shops[i], 1>
		if gift ∉pocket
			then if pocket 中礼品个数 = M
				then POP(pocket)
					discard ← discard +1
				else PUSH(pocket, gift)
			else 将 pocket 中与 gift 编号相同的礼品 L 增加 1
				调用 LIFT-UP 维护 pocket 的堆性质
return discard

算法 3-11 解决“David 购物问题”一个案例的过程

设一个案例中的店铺数为 n,衣袋容量(可装下的礼品数)为 m。算法 3-11 的第 3~11行的 for 循环将重复 n 次。其中的第 5 行涉及在优先队列 pocket 的数据堆(数组)中查找,耗时为Θ (m),第 7、9 行对优先队列 pocket 的入队和出队操作及第 11 行的 LIFT-UP 操作耗时均为Θ (lgm)。由此可见,算法 3-11 的运行时间为Θ (nm)。解决本问题的算法的 C++实现代码存储于文件夹 laboratory/David Shopping 中,读者可打开文件 David Shopping.cpp 研读,并试运行之。C++代码的解析请阅读第 9 章 9.3.2 节中程序 9-35~程序 9-37 的说明。

#ifndef _PRIORITYQUEUE_H
#define _PRIORITYQUEUE_H
#include <vector>
#include <functional>
using namespace std;
template<typename T, typename Compare = less<T>>
class PriorityQueue
{
private:
    vector<T> heap;
    int indexOfMost(int i);
    void siftDown(int index);
    void liftUp(int index);
public:
    bool empty();
    int size();
    T top();
    void pop();
    void push(T x);
    int search(T &x);
    void replace(T x, int i);
};
template<typename T, typename Compare>
int PriorityQueue<T, Compare>::indexOfMost(int i)
{
    int heapSize = size();
    int j = i;
    if (2 * i + 1 < heapSize && Compare()(heap[i], heap[2 * i + 1]))
        j = 2 * i + 1;
    if (2 * (i + 1) < heapSize && Compare()(heap[j], heap[2 * (i + 1)]))
        j = 2 * (i + 1);
    return j;
}
template<typename T, typename Compare>
void PriorityQueue<T, Compare>::siftDown(int index)
{
    int i = index, j = indexOfMost(index);
    while (i != j)
    {
        swap(heap[i], heap[j]);
        i = j;
        j = indexOfMost(i);
    }
}
template<typename T, typename Compare>
void PriorityQueue<T, Compare>::liftUp(int index)
{
    int i = index;
    int j = (i % 2) ? (i - 1) / 2 : (i / 2);
    while (i > 0 && Compare()(heap[j], heap[i]))
    {
        swap(heap[i], heap[j]);
        i = j;
        j = (i % 2) ? (i - 1) / 2 : (i / 2);
    }
}
template<typename T, typename Compare>
bool PriorityQueue<T, Compare>::empty()
{
    return heap.empty();
}
template<typename T, typename Compare>
int PriorityQueue<T, Compare>::size()
{
    return heap.size();
}
template<typename T, typename Compare>
T PriorityQueue<T, Compare>::top()
{
    return heap[0];
}
template<typename T, typename Compare>
void PriorityQueue<T, Compare>::push(T x)
{
    int heapSize = size();
    heap.push_back(x);
    liftUp(heapSize);
}
template<typename T, typename Compare>
void PriorityQueue<T, Compare>::pop()
{
    if (!empty())
    {
        int heapSize = size() - 1;
        heap[0] = heap[heapSize];
        heap.erase(heap.end() - 1);
        siftDown(0);
    }
}
template<typename T, typename Compare>
int PriorityQueue<T, Compare>::search(T &x)
{
    int n = size();
    for (int i = 0; i < n; i++)
        if (heap[i] == x)
        {
            x = heap[i];
            return i;
        }
    return -1;
}
template<typename T, typename Compare>
void PriorityQueue<T, Compare>::replace(T x, int i)
{
    T y = heap[i];
    heap[i] = x;
    if (Compare()(y, x))
    {
        liftUp(i);
        return;
    }
    if (Compare()(x, y))
        siftDown(i);
}
#endif /*_PRIORITYQUEUE_H*/

问题 3-8 内存分配

描述

内存是计算机的重要资源之一,程序运行的过程中必须对内存进行分配。操作系统经典的内存分配过程是这样进行的:1 内存以内存单元为基本单位,每个内存单元用一个固定的整数作为标识,称为地址。地址从 0 开始连续排列,地址相邻的内存单元被认为是逻辑上连续的。我们把从地址 i 开始的 s 个连续的内存单元称为首地址为 i、长度为 s 的地址片。2 运行过程中有若干进程需要占用内存,对于每个进程有一个申请时刻 T,需要内存单元数 M 及运行时间 P。在运行时间 P 内(即 T 时刻开始,T+P 时刻结束) ,这 M 个被占用的内存单元不能再被其他进程使用。3 假设在 T 时刻有一个进程申请 M 个单元,且运行时间为 P,则:y 若 T 时刻内存中存在长度为 M 的空闲地址片,则系统将这 M 个空闲单元分配给该进程。若存在多个长度为 M 个空闲地址片,则系统将首地址最小的那个空闲地址片分配给该进程。y 如果 T 时刻不存在长度为 M 的空闲地址片,则该进程被放入一个等待队列。对于处于等待队列队头的进程,只要在任一时刻,存在长度为 M 的空闲地址片,系统马上将该进程取出队列,并为它分配内存单元。注意,在进行内存分配处理过程中,处于等待队列队头的进程的处理优先级最高,队列中的其他进程不能先于队头进程被处理。现在给出一系列描述进程的数据,请编写一程序模拟系统分配内存的过程。

输入

第一行是一个数 N,表示总内存单元数(即地址范围从 0 到 N-1)。从第二行开始每行包含描述一个进程的三个整数 T、M、P(M ≤N)。最后一行用三个 0 表示结束。
数据已按 T 从小到大排序。
输入文件最多 10000 行,且所有数据都小于 109。
输入文件中同一行相邻两项之间用一个或多个空格隔开。

输出
包括 2 行。
第一行是全部进程都运行完毕的时刻。
第二行是被放入过等待队列的进程总数。
输入样例
10
1 3 10
2 4 3
3 4 4
4 1 4
5 3 4
0 0 0
输入样例
12
2

解题思路

(1)数据的输入与输出

根据输入文件格式,首先读取内存单元数 N。然后依次读取每个任务的申请时刻 T,需要内存单元数 M 及运行时间 P。直至输入结束标志 T=M=P=0。将这些任务的数据组织成数组 a。对输入数据 N 和 a,计算完成所有任务的时刻 time 和完成所有任务过程中进入等待队列的进程个数 count。将两个计算结果分别作为一行写入输出文件。

打开输入文件 inputdata
创建输出文件 outputdata
创建数组 a←∅
从 inputdata 中读取 N
从 inputdata 中读取 T, M, P
	 while T>0 or M>0 or P>0
		do APPEND(a, (T, M, P))
			从 inputdata 中读取 T, M, P
(time, count)←MEMORY-ALLOC(N, a)
将 time 和 count 各作为一行写入 outputdata
关闭 inputdata
关闭 outpudata

其中,第 9 行调用计算完成所有任务的时间和等待进程个数的过程 MEMORY-ALLOC(N,a)是解决一个案例的关键。

(2)处理一个案例的算法过程

测试案例中各进程占据内存的情形如图 3-10 所示。

在这里插入图片描述

设置一个计数器 time(初始化为 1) ,利用一个循环模拟时钟:每重复一次,time 自增 1。设置一个用来登记运行中进程的集合(优先队列)P,其中的元素<finish_time, start_addr,end_addr>记录进程的完成时间和所占内存的起、止地址,该集合初始化为{<time+ p[a[1]], 0,m[a[1]]-1>},即包含第一个登录的程序 a[1]生成的进程。设置一个进程等待队列(先进先出) Q,其中的元素<time_length, mem_length>记录等待进程的运行时间长度和所需内存长度,队列初始化为∅。还要设置一个内存片表 S,其中的元素<start_addr, end_addr>记录内存片的起止地址(初始化为仅含一个元素:< m[a[1]], N−1>,即整个内存分配给第一个进程后剩余的部分) 。S 中元素应按首地址升序有序,才便于在其中查找首地址最小的合适地址片。输入中的各程序数据<t, m,p>存储于数组 a[1…n]中,用 current 表示 a 中当前即将登录的进程编号(初始化为 2) 。为得到正确的输出,模拟过程还要维护一个变量 count(初始化为 0) ,表示进入等待队列 Q 的进程数。在模拟过程中,每当时钟 time 增长 1 秒,检测 P 中队首是否在 time 时间完成运行的进程。若是,则将队首出队,并释放内存。然后检测 Q 中是否有等待进程。若是,则将队首出队并进行与上述对及时登录的 a[current]相同的分配内存(修改 S)及投入运行(修改 P)的操作。接着检测 a[current]的登录时间是否等于 time。若是,则检测 S 中是否有足够大的内存片提供给该进程。若是,则为该进程分配内存,并计算完成时间,将其加入到 P 中。若此时 S 中无足够内存供该进程使用,则将该进程加入队列 Q(count 自增 1)。当 P=∅时,结束模拟。此时 time 和 count 即为所求。模拟过程可表示成如下的伪代码。

MEMORY-ALLOC(N, a)
n← length [a], time←1, count←0, current←2
P←{<time+p[a[1]], 0, m[a[1]]-1>}, Q←∅, S←{<m[a[1]], N-1>}
while P≠∅
	do time← time+1
		while P≠∅
			do <finish_time, start_addr, end_addr>TOP(P)
				if finish_time=time
					then FREE-ADDRESS(S, <start_addr, end_addr>)
						POP(P)
					else break this loop
		while Q≠∅
			do <time_length, mem_length>TOP(Q)
				start_addr←ALLOC-ADDRESS(S, mem_length)
					if start_addr≥0
						then POP(Q)
							PUSH(P, <time+time_length, start_addr, start_addr+mem_length >)
						else break this loop
	if current≤n
		then if t[a[current]] =time
				then start_addr←ALLOC-ADDRESS(S, m[a[current]])
					if start_addr≥0
						then PUSH(P, <time+p[a[current]], start_addr,
						start_addr +m[a[current]]>)
						else PUSH(Q, <p[a[current]], m[a[current]]>)
							count←count+1
						current←current+1
return time, count

算法 3-12 解决“内存分配”问题的算法过程

算法 3-12 的第 3~25 行的 while 循环模拟时钟,每次重复 time 增加 1(第 4 行)。第5~10 行的 while 循环完成对 P 中此时运行完毕进程的检测与处理。之所以用循环,是因为可能有若干个进程同时运行完毕。由于 P 是一个优先队列(完成时间是优先级),因此一旦检测到队首元素未完成运行,即可判定队列内无此时完成运行的进程(第 10 行),退出此循环。第 11~17 行的 while 循环检测处理 Q 中等待进程是否能得到足够的内存。之所以用循环,是因为有可能 S 中的内存片可满足多个等待进程的内存需求。由于队列中元素满足先进先出规则,故一旦队首检测失败,则退出此循环(第 17 行)。第 18~25行,检测处理下一个登录的程序是否到达。若用基于平衡二叉搜索树来表示地址片集合S,则算法 3-12 中第 8 行的 FREE- ADDRESS 过程和第 13、20 行的 ALLOC-ADDRESS过程可描述如下。

ALLOC-ADDRESS(S, m_length)Z在地址片集合 S 中查找首地址最小长度不小于 m_length 的地址片
for each <start_addr, end_addr>∈S Z按节点的中序遍历顺序
	do if end_addr-start_addr+1≥ m_length
		then DELETE(S, <start_addr, end_addr>)
			if end_addr-start_addr+1>m_length
				then INSERT(S, <start_addr+m_length, end_addr>)
			return start_addr
return -1
FREE-ADDRESS(S, <s_addr, e_addr>)
for each <start_addr, end_addr >∈S
	do if end_addr+1=s_addr or start_addr=e_addr+1
		then DELETE(S, <start_addr, end_addr >)
			if end_addr+1=s_addr
				then INSERT(S, <start_addr, e_addr>)
				else INSERT(S, <s_addr, end_addr>)
			return
INSERT(S, <s_addr, e_addr>)

算法 3-13 在基于平衡二叉搜索树的地址片集合 S 中申请分配过程和释放地址过程。算法 3-13 中的 ALLOC-ADDRESS 过程在 S 中查找长度不小于 m_length、 首地址最小(题面要求之一)的地址片,若找到,则修改该地址片的首地址,并返回原首地址。若 S 中不存在满足条件的地址片,则返回-1。这是因为正常的地址不会小于 0。由于 S 是基于平衡二叉搜索树的集合,所以第 1~6 行的 for 循环按中序遍历顺序依次检测,第一个满足条件的地址片就是首地址最小的满足条件者。取出满足条件的地址片(第 3 行),若其长度大于需求(第 4 行),则将该地址片的首地址修改为原首地址加上指定长度 start_ addr+m_length,将剩余部分放回 S(第 5 行)。FREE-ADDRESS 过程,将指定的地址片<s_addr, e_addr>放回到集合 S 中。需要检测 S 中是否有地址片<start_addr, end_addr>可与<s_addr, e_addr>连成一片。这由第 1~7 行的 for 循环完成。连接有两种可能<start_addr, end_addr>在前或在后(第 2 行)。若有这样的地址片,将其从 S 中取出(第 3 行)。若该地址片在前,则修改其终止地址为e_addr,并重新加入 S(第 5 行)。若该地址片在后,则修改其开始地址为 s_addr,加入S(第 6 行)。如果 S 中没有可与指定地址片<s_addr, e_addr>连接者,直接将该地址片加入 S(第 8 行)。如果案例中程序数为 x,则 S 中元素(地址片)个数为Θ(x)。所以,以上两个过程的循环重复次数为Θ(x)。每次重复,需要执行对 S 的删除、插入操作。根据表 2-1,对平衡搜索树的这样的操作耗时Θ(lgx)。因此,算法 3-13 中的两个过程运行时间为Θ(xlgx)。回到算法 3-12。第 3~25 行的 while 循环,其重复次数取决于输入中的各程序登录的时间、在机器中运行的时间和内存量的大小等各种因素。假定该循环的重复次数为 y,程序数为 x,每个程序作为运行着的进程进入且只进入 P 一次,所以第 6~10 行的操作总的执行次数必为Θ(x)。每次执行都要调用对 S 的释放地址片操作和对优先队列 P 的出队操作,前者耗时Θ(xlgx),后者耗时Θ(lgx)。因此,这些操作消耗的时间为Θ(x2 lgx)。第 13~18 行的操作也恰重复 x 次。每次重复都要进行对 S 申请地址片的操作(耗时Θ(xlgx))和对 P 的入队操作(耗时Θ(lgx))或对 Q 的入队操作(耗时Θ(1)) 。因此,这些操作消耗的时间为Θ(x2lgx)。最后,考虑第 20~25 行的操作,由于进入 Q 的进程至多只有 x 个,所以这部分操作重复Θ(x)次。每次重复都要执行对 S 的申请地址片操作(耗时Θ(xlgx)) ,以及可能对 P 的入队操作(耗时Θ(lgx)) 、对 Q 的出队操作(耗时Θ(1)) ,故这些操作的总的时间也是Θ(x2 lgx)。除了这些操作以外,还有重复 y 次的常数时间(简单的比较判断、赋值、算术运算等)操作。所以算法3-12 的运行时间为Θ(x2 lgx)+Θ(y)。解决本问题的算法的 C++实现代码存储于文件夹 laboratory/Memory Allocate 中,读者可打开文件 Memory Allocate.cpp 研读,并试运行之。C++代码的解析请阅读第 9 章中程序 9-6~程序 9-8 的说明。

#include <fstream>
#include <iostream>
#include <vector>
#include <queue>
#include <set>
#include "../utility/PriorityQueue.h"
using namespace std;
struct Program
{
    int t, m, p;
    Program(int T, int M, int P): t(T), m(M), p(P) {}
};
struct AddressPice
{
    int startAddr, endAddr;
    AddressPice(int s, int e): startAddr(s), endAddr(e) {}
    const int length()
    {
        return endAddr - startAddr + 1;
    }
};
bool operator==(const AddressPice &a, const AddressPice &b)
{
    return a.startAddr == b.startAddr;
}
bool operator<(const AddressPice &a, const AddressPice &b)
{
    return a.startAddr < b.startAddr;
}
struct WaitProgress
{
    int timeLength;
    int memLength;
    WaitProgress(int t, int m): timeLength(t), memLength(m) {}
};
bool operator==(const WaitProgress &a, const WaitProgress &b)
{
    return a.timeLength == b.timeLength && a.memLength == b.memLength;
}
bool operator<(const WaitProgress &a, const WaitProgress &b)
{
    return a.timeLength < b.timeLength;
}
struct Progress
{
    int finishTime;
    AddressPice mem;
    Progress(int ft, AddressPice m): finishTime(ft), mem(m) {}
};
bool operator==(const Progress &a, const Progress &b)
{
    return a.finishTime == b.finishTime;
}
bool operator>(const Progress &a, const Progress &b)
{
    return a.finishTime > b.finishTime;
}
void freeAddress(set<AddressPice> &S, Progress a)
{
    set<AddressPice>::iterator addr;
    for (addr = S.begin(); addr != S.end(); addr++) //扫描内存片集合
    {
        if (addr->endAddr + 1 == a.mem.startAddr) //a接在addr之后
        {
            AddressPice x = *addr;
            S.erase(addr);
            x.endAddr = a.mem.endAddr;
            S.insert(x);
            return;
        }
        else if (addr->startAddr - 1 == a.mem.endAddr) //addr接在a之后
        {
            AddressPice x = *addr;
            S.erase(addr);
            x.startAddr = a.mem.startAddr;
            S.insert(x);
            return;
        }
    }
    S.insert(a.mem);
}
set<AddressPice>::iterator allocAddress(set<AddressPice> &S, int length)
{
    set<AddressPice>::iterator addr;
    for (addr = S.begin(); addr != S.end(); addr++)
    {
        if (addr->endAddr - addr->startAddr + 1 >= length)
        {
            break;
        }
    }
    return addr;
}
pair<int, int> memoryAlloc(vector<Program> &a, int N)
{
    int time = 1, count = 0, current = 1;
    queue<WaitProgress> Q;
    set<AddressPice> S;
    PriorityQueue<Progress, greater<Progress>> P;
    P.push(Progress(time + a[0].p, AddressPice(0, a[0].m - 1)));
    S.insert(AddressPice(a[0].m, N - 1));
    while (!P.empty())
    {
        time++;
        while (!P.empty()) //运行完毕的进程释放内存片
        {
            Progress p = P.top();
            if (p.finishTime == time) //p运行完毕
            {
                freeAddress(S, p);
                P.pop();
            }
            else
            {
                break;
            }
        }
        while (!Q.empty())  //等待队列
        {
            WaitProgress p = Q.front();
            set<AddressPice>::iterator addr = allocAddress(S, p.memLength);
            if (addr != S.end() ) //申请到首地址最小的足量地址片
            {
                Q.pop();//p出队
                if ((addr->endAddr - addr->startAddr + 1) > p.memLength) //分配给p后还有余量
                {
                    int start = addr->startAddr + p.memLength + 1; //剩余的首地址
                    int end = addr->endAddr; //尾地址
                    S.insert(AddressPice(start, end));//剩余片插入S
                }
                int finishTime = time + p.timeLength; //运行完成时间
                AddressPice addressPice = AddressPice(addr->startAddr, addr->startAddr + p.memLength - 1); //所占地址片
                P.push(Progress(finishTime, addressPice));//加入运行
                S.erase(addr);//取出该地址片
            }
            else  //内存不够
            {
                break;
            }
        }
        if (current < a.size())
        {
            if (a[current].t == time)
            {
                set<AddressPice>::iterator addr = allocAddress(S, a[current].m);
                if (addr != S.end())  //申请到首地址最小的足量地址片
                {
                    if (addr->endAddr - addr->startAddr + 1 > a[current].m)
                    {
                        int start = addr->startAddr + a[current].m;
                        int end = addr->endAddr;
                        S.insert(AddressPice(start, end));
                    }
                    int finishTime = time + a[current].p;
                    AddressPice addressPice = AddressPice(addr->startAddr, addr->startAddr + a[current].m - 1);
                    P.push(Progress(finishTime, addressPice));
                    S.erase(addr);
                }
                else
                {
                    Q.push(WaitProgress(a[current].p, a[current].m));
                    count++;
                }
                current++;
            }
        }
    }
    return make_pair(time, count);
}
int main()
{
    ifstream inputdata("inputdata.txt");
    ofstream outputdata("outputdata.txt");
    int N, T, M, P;
    inputdata >> N;
    inputdata >> T >> M >> P;
    vector<Program> a = vector<Program>();
    while (T || M || P)
    {
        a.push_back(Program(T, M, P));
        inputdata >> T >> M >> P;
    }
    pair<int, int> result = memoryAlloc(a, N);
    cout << result.first << endl;
    cout << result.second << endl;
    outputdata << result.first << endl;
    outputdata << result.second << endl;
    inputdata.close();
    outputdata.close();
    return 0;
}

问题 3-10 符号导数

写一个程序能对给定的函数 f(x)计算它的符号导数 f’(x) = df(x)/dx。函数由包含下列运算符的表达式定义:+(加),−(减),
*(乘),/(除)及 ln(自然对数)。表达式中的运算数可以是变量 x 也可以是数值常量。表达式中还有嵌套的括弧( )表示的子表达式。表达式以常见的中缀式表示。例如: ( 2 ∗ l n ( x + 1.7 ) − x ∗ x ) / ( ( − 7 ) + 3.2 ∗ x ∗ x ) + ( x + 3 ∗ x ) ∗ x (2*ln(x+1.7)-x*x)/((-7)+3.2*x*x)+(x+3*x)*x (2ln(x+1.7)xx)/((7)+3.2xx)+(x+3x)x数值常量的格式为 d.d,并可带有符号(+或−)。数值常量是否带有小数部分是任意的。输入的表达式保证是正确的(不会发生语法问题)。输出的表达式也应该是中缀式。为便于编程,表达式中可包含未化简项如 0x、 1x、 0+x,等等。导数按下列规则计算:
1 运算符*及/的优先级高于+、−。括号可改变运算符的优先级。
2 运算符+、−、 ∗ * 及 / 是左结合的。即按从左到右的顺序进行计算的(如: a ∗ b ∗ c = ( a ∗ b ) ∗ c , a / b / c = ( a / b ) / c , a / b ∗ c = ( a / b ) ∗ c a*b*c=(a*b)*c,a/b/c = (a/b)/c,a/b*c = (a/b)*c abc=(ab)c,a/b/c=(a/b)/c,a/bc=(a/b)c,等等)。
3 求导公式为:
(a + b)’ = a’ + b’
(a − b)’ = a’ − b’
(a * b)’ = (a’ * b + a * b’)
(a / b)’ = (a’ * b − a * b’) / b^2 注意:使用 b^2 而非(b*b)表示幂
ln(a)’ = (a’)/(a)
x’ = 1
常量’ = 0
4 计算符号导数时,用上述规则给输出表达式加括号,无须处理表达式的简化,即 0 ∗ a = 0 , 1 ∗ a = a 0*a = 0,1*a = a 0a=0,1a=a, 等等。
输入
输入文件中每行定义一个函数 f(x)。输入的各行不包含空格。
输出
对应每一个函数 f,输出一行 f’=df/dx。 表示 f(x) 及 f’(x)的字符串所含字符保证不超过 100。
输入样例
x ∗ x / x x*x/x xx/x
− 45.78 ∗ x + x -45.78*x+x 45.78x+x
− 2.45 ∗ x ∗ x + l n ( x − 3 ) -2.45*x*x+ln(x-3) 2.45xx+ln(x3)
输出样例
( ( 1 ∗ x + x ∗ 1 ) ∗ x − x ∗ x ∗ 1 ) / x 2 ((1*x+x*1)*x-x*x*1)/x^2 ((1x+x1)xxx1)/x2
0 ∗ x − 45.78 ∗ 1 + 1 0*x-45.78*1+1 0x45.781+1
( 0 ∗ x − 2.45 ∗ 1 ) ∗ x − 2.45 ∗ x ∗ 1 + ( 1 − 0 ) / ( x − 3 ) (0*x-2.45*1)*x-2.45*x*1+(1-0)/(x-3) (0x2.451)x2.45x1+(10)/(x3)

解题思路

(1)数据的输入与输出

根据输入文件的格式,依次从中读取每个案例的一行表示函数中缀式的 s,计算 s 的导函数 deriv,将 deriv 作为一行写入输出文件。循环往复,直至不能从输入文件读取到 s。

打开输入文件 inputdata
创建输出文件 outputdata
while 能从 inputdata 中读取一行到 s
	do deriv←SYMBLE-DERIVATION(s)
		将 deriv 作为一行写入 outputdata
关闭 inputdata
关闭 outpudata

其中,第 4 行调用计算表达式 s 的导函数的过程 SYMBLE-DERIVATION(s)是解决一个案例的关键。

(2)处理一个案例的算法过程

如果能将中缀表达式串 s 表示成二叉树,就可以利用二叉树结构的递归性(孩子也是二d叉树),递归地计算孩子的导数,根据求导公式合成导函数表达式。递归不会无限进行,因d为作为叶子节点,变量和常量的导数可直接算得。以表达式 x ∗ x + x x*x+x xx+x 为例,根节点“+”的左d孩子 x ∗ x x*x xx。表达式“ x ∗ x x*x xx”的父节点为“ ∗ * ”,左右孩子均为变量“x”,导数为 1,根据积的导d数公式,可以得到“ 1 ∗ x + x ∗ 1 1*x+x*1 1x+x1”d 。根节点的右孩子“x”的导数为 1,根据和的导数公式,得到d“ 1 ∗ x + x ∗ 1 + 1 1*x+x*1+1 1x+x1+1”(见图 3-12)。

在这里插入图片描述

于是,对本问题输入中的一个案例——一个用字符串表示的中缀表达式(运算数位于运算符的两侧)s,先将其转换成一棵对应的二叉树 exp。然后对 exp 进行求导操作,得到表示导函数表达式的二叉树 deriv。最后对 deriv 做中序遍历将其转换成字符串输出。设表达式二叉树的节点包含一个表示运算符的属性 ope 及两个分别指向左运算数和右运算数的指针 lopd 和 ropd,并假定过程 EXPRESSION(ope, lopd, ropd)生成这样的节点。先来考虑如何将一个表示中缀表达式的字符串 s 转换成该表达式的二叉树表示。首先需要先对表达式中所有运算符明确各自的运算优先级。在本问题中,涉及的运算符只有 “_”
“ln”
“*”
“/”
“+”
“−”,优先级分别设置为 6、5、4、4、3、3。其中,下划线“_”表示一元运算“−”,由于该运算的优先级高于其他运算,当然也高于二元运算“−”,所以用特殊的下划线表示,以示区别。表达式串中还有两个符号“(”和“)”,它们并不实际进行运算,而是用来改变运算优先级的。为便于处理,我们也赋予它们特殊的优先级分别是 1 和2。此外,用特殊字符“@”来标识表达式串的结束,给它赋予优先级−1。我们把这些符号连同它们的优先级保存在集合 priority 中。在解析字符串 s 前,需要对其进行预处理:在其尾部追加特殊符号“@”,并将一元运算符“−”替换为下划线“_”。中缀表达式预处理过程可以描述为如下过程。

PREPROCESSING(s)
n←length[s]
for i←1 to n
	do if s[i]为一元运算符"-"
		then s[i]"_"
APPEND(s, '@')

算法 3-16 中缀表达式预处理过程

显然,算法 3-16 的运行时间为Θ(n)。为解析经过预处理的字符串 s 中的表达式,设置两个栈:运算符栈 oper(为便于对运算符的处理,oper 中预先压入“@”)和运算数栈 oprands。从字符串首部开始扫描,读取一项 item。若 item 为常量或变量,将其压入栈 oprands 中。否则,item 是一个运算符。若 item 为“(”,则直接将其压入 oper 栈中。否则,检测 oper 栈顶的 t 表示的运算符优先级是否不小于 item 的优先级。若是,则从 oprands 弹出左、右运算数,合成表达式后压入 opd 栈。重复这样的操作,直至 item 的优先级高于 oper 栈顶运算符的优先级。此时,若 item 为“)”则 oper 栈顶 t 必为“(”。这其实意味着完成一个圆括弧括起来的子表达式的转换,于是,从 oper 栈中弹出“(”。否则,item 表示一个真正的运算符,将其压入 oper 栈。循环执行这一过程,直至 item 读到“@”位置。这一过程可表示成如下所示的伪代码。

TO-EXPRESSION(s)
oper←∅, oprands←∅
PUSH(oper, "@")
while true
	do if s 扫描完毕 then 终止循环
		item←s 中一项
		if item 为常量或变量 x
			then PUSH(operands, item)
				进入循环的下一轮重复
		if item=" ("
			then PUSH(oper, item)
				进入循环的下一轮重复
		t←TOP(oper)
		while priority[t]>1 and priority[t]≥priority[item]
			do re←POP(operands)
				if item="ln"or item="_"
					then le←NIL
					else le←POP(operands)
				PUSH(operands, EXPRESSION(item, le, re))
				POP(oper)
				t←TOP(oper)
			if item=")"
			then POP(oper)
			else PUSH(oper, item)
return TOP(operands)

算法 3-17 中缀表达式串转换二叉树过程

设中缀表达式串 s 的长度为 n,由于 TO-EXPRESSION 过程的第 3~23 行本质上就是对s 进行扫描,故运行时间 T(n)=Θ(n)。

一旦将表达式表示成二叉树 exp,如前所述利用二叉树结构的递归性和各种运算的导数公式,就可计算出表示导数表达式的二叉树。伪代码过程描述如下。

DERIVATION(exp)if lopd[exp]≠NIL	then left← DERIVATION(lopd[exp])	else left←NILif lopd[exp]≠NI	then right← DERIVATION(lopd[exp])	else right←NILif ope[exp]= "+"or ope[exp]= "-"	then return EXPRESSION(ope[exp], left, right)if ope[exp]= "*"	 then return EXPRESSION("+", EXPRESSION("*", left, ropd[exp]),								 EXPRESSION("*", lopd[exp], right))if ope[exp]= "/"	then return EXPRESSION("/", EXPRESSION("-", EXPRESSION("*", left, ropd[exp]), 												EXPRESSION("*", lopd[exp], right)), 												EXPRESSION("^", ropd, EXPRESSION("2", NIL, NIL)))if ope[exp]= "/"	then return EXPRESSION("/", right, ropd[exp])if ope[exp]= "ln"	then return EXPRESSION("/", EXPRESSION("1", NIL, NIL), right)if ope[exp]= "_"	then if ope[ropd[exp]]为常数			then return EXPRESSION("0", NIL, NIL)			if ope[ropd[exp]]= "x"				then return EXPRESSION("-1", NIL, NIL)			return EXPRESSION(ope[exp], NIL, right)if ope[exp]= "x"	then return EXPRESSION("1", NIL, NIL)return EXPRESSION("0", NIL, NIL)

算法 3-18 计算表示成二叉树的表达式导数过程

设 exp 有 n 个节点,会被递归调用 n 次,每次至多调用三次 EXPRESSION 生成一棵二叉树。因此运行时间 T(n)=Θ(n)。对表达式 exp 调用算法 3-18 的过程 DERIVATION(exp),返回一棵表示导数表达式的二叉树 deriv。我们需要对 deriv 做与 TO-EXPRESSION 过程相反的计算,转换成中缀表达式串。这只要对 deriv 做一次中序遍历就可实现。在这个过程中需要注意的是,如果子树表示的运算优先级低于当前运算的优先级,子树对应的子串需加上括号。写成伪代码过程如下。

TO-STRING(deriv)
s←""
if lopd[deriv]≠NIL
then add-parentheses←lopd[deriv]非常数亦非变量 and priority[ope[deriv]]>
    priority[ope[lopd[deriv]]]
        if add-parentheses=TRUE
            then s←s+" ("
        s←s+TO-STRING(lopd[derive])
        if add-parentheses=TRUE
            then s←s+")"
s←s+TO-STRING(ope[derive])
if ropd[deriv]≠NIL
then add-parentheses←ropd[deriv]非常数亦非变量 and priority[ope[deriv]]>
    priority[ope[ropd[deriv]]]
        if add-parentheses=TRUE
            then s←s+" ("
        s←s+TO-STRING(ropd[derive])
        if add-parentheses=TRUE
            then s←s+")"
return s

算法 3-19 将表达式二叉树转换成中缀表达式串的过程

算法 3-19 中的过程 TO-STRING 本质上就是对二叉树 deriv 进行中序遍历。其中第 2~8 行处理非空左子树,第 9 行处理根,第 10~16 行处理非空右子树。子树若非常量或变量,且运算优先级低于本层的运算,则需加括号。这个检测条件分别由第 3 行(左子树)和第 11 行(右子树)的 add-parentheses 表示。该算法的运行时间取决于表达式 deriv 的高度Θ(h)。二叉树极端的情形之一是所有的节点均至多只有一个孩子,这时,树的高度即为节点数 n。由于这两个操作之一对访问到的每一个节点都要进行,因此,算法 3-19的运行时间为Θ (n2)。

利用算法 3-16~算法 3-19 我们有如下所示的计算中缀表达式 s 的导函数的中缀表达式的算法。

SYMBLE-DERIVATION(s)
PREPROCESSING(s)
exp←TO-EXPRESSION(s)
deriv← DERIVATION(exp)
s← TO-STRING(deriv)
FIX(s)
return s

算法 3-20 计算函数中缀表达式 s 的导函数中缀表达式的算法过程

设中缀表达式 s 中有 n 个项,由算法 3-16~算法 3-19 的分析可知,第 1~3 行耗时均为Θ(n),第 4 行耗时为Θ (n 2)。第 5 行的 FIX 过程是将导数前缀式串中的“_”消除掉。这只需Θ(n)的时间。于是,算法 3-20 的运行时间为 O(n2)。解决本问题的算法的 C++实现代码存储于文件夹 laboratory/Symble Derivation 中,读者可打开文件 Symble Derivation.cpp 研读,并试运行之。C++代码的解析请阅读第 9 章 9.2.2节中程序 9-13~程序 9-23 的说明。本章我们讨论了解决现实模拟问题的 5 种基本方法。问题 3-1 和问题 3-2 利用的是简单模拟——即通过循环模拟事物分阶段发展的过程。问题 3-3 和问题 3-4 利用栈来模拟对象先进后出的发展过程。问题 3-5 和问题 3-6 利用队列模拟事物先来先服务的发展过程。问题 3-7和问题 3-8 利用优先队列模拟按事物的等级决定服务顺序的发展过程。问题 3-9 和问题 3-10给出了用二叉树表示数学表达式模拟数学计算的方法。

#include <iostream>
#include <string>
#include <stack>
#include <fstream>
#include <hash_map>
using namespace std;
#include <string.h>
pair<string, int> a[] = {make_pair("(", 1), make_pair(")", 2),
                         make_pair("ln", 5), make_pair("*", 4),
                         make_pair("/", 4), make_pair("+", 3),
                         make_pair("-", 3), make_pair("@", -1),
                         make_pair("_", 6), make_pair("^", 7)
                        };
hash_map<string, int> priority(a, a + 10);
void preprocess(string &s)
{
    size_t n = s.length();
    for (int i = 0; i < n; i++)
        if ((i == 0 && s[0] == '-') || (s[i] == '-' && !isdigit(s[i - 1]) && s[i - 1] != 'x' && s[i - 1] != ')'))
            s[i] = '_';
    s += '@';
}
void fix(string &s)
{
    int i = 0;
    while (i < s.length())
    {
        if ((s[i] == '+' && s[i + 1] == '_') || (s[i] == '_' && s[i + 1] == '+'))
        {
            s.erase(i, 1);
            s[i] = '-';
        }
        else if (s[i] == '-' && s[i + 1] == '_' || s[i] == '_' && s[i + 1] == '-')
        {
            s[i] = '+';
            s.erase(i + 1, 1);
        }
        else if (s[i] == '_')
            s[i] = '-';
        i++;
    }
}
class Expression
{
protected:
    Expression *lopd;
    Expression *ropd;
public:
    string ope;
    Expression(string op, Expression *l = NULL, Expression *r = NULL): ope(op), lopd(l), ropd(r) {}
    ~Expression()
    {
        if (lopd)
        {
            delete lopd;
            lopd = NULL;
        }
        if (ropd)
        {
            delete ropd;
            ropd = NULL;
        }
    }
    //string toString();
    virtual Expression *copy() = 0;
    void toString(string &s);
    virtual Expression *derivation() = 0;
};
//string Expression::toString(){
//    string s;
//    if(lopd){
//      bool add_parences=(priority[lopd->ope]>0) && ( priority[lopd->ope]<priority[ope]);
//      s+=add_parences ? ("("+lopd->toString()+")") : lopd->toString();
//  }
//    s+=ope;
//  if(ropd){
//      bool add_parences=(priority[ropd->ope]>0) && (priority[ropd->ope]<priority[ope]);
//      s+=add_parences ? ("("+ropd->toString()+")") : ropd->toString();
//  }
//    return s;
//}
void Expression::toString(string &s)
{
    string ls, rs;
    if (lopd)
    {
        lopd->toString(ls);
        bool add_parences = (priority[lopd->ope] > 0) && ( priority[lopd->ope] < priority[ope]);
        if (add_parences)
            ls = '(' + ls + ')';
    }
    if (ropd)
    {
        ropd->toString(rs);
        bool add_parences = (priority[ropd->ope] > 0) && (priority[ropd->ope] < priority[ope]);
        if (add_parences)
            rs = '(' + rs + ')';
    }
    s = ls + ope + rs;
}

class Const: public Expression
{
public:
    Const(string val): Expression(val) {}
    Expression *copy()
    {
        return new Const(ope);
    }
    Expression *derivation()
    {
        return new Const("0");
    }
};
class Varible: public Expression
{
public:
    Varible(): Expression("x") {}
    Expression *copy()
    {
        return new Varible();
    }
    Expression *derivation()
    {
        return new Const("1");
    }
};
class Sum: public Expression
{
public:
    Sum(Expression *left, Expression *right): Expression("+", left, right) {}
    Expression *derivation();
    Expression *copy()
    {
        return new Sum(lopd->copy(), ropd->copy());
    }
};
Sum *operator+(Expression &a, Expression &b)
{
    return new Sum(&a, &b);
}
Expression *Sum::derivation()
{
    Expression &du = *(lopd->derivation()), &dv = *(ropd->derivation());
    return du + dv;
}
class Difference: public Expression
{
public:
    Difference(Expression *left, Expression *right): Expression("-", left, right) {}
    Expression *copy()
    {
        return new Difference(lopd->copy(), ropd->copy());
    }
    Expression *derivation();
};
Difference *operator-(Expression &a, Expression &b)
{
    return new Difference(&a, &b);
}
Expression *Difference::derivation()
{
    Expression &du = *(lopd->derivation()), &dv = *(ropd->derivation());
    return du - dv;
}
class Product: public Expression
{
public:
    Product(Expression *left, Expression *right): Expression("*", left, right) {}
    Expression *copy()
    {
        return new Product(lopd->copy(), ropd->copy());
    }
    Expression *derivation();
};
Product *operator*(Expression &a, Expression &b)
{
    return new Product(&a, &b);
}
Expression *Product::derivation()
{
    Expression &u = *(lopd->copy()), &v = *(ropd->copy());
    Expression &du = *(u.derivation()), &dv = *(v.derivation());
    return *(du * v) + *(u * dv);
}
class Power2: public Expression
{
public:
    Power2(Expression *left): Expression("^", left, new Const("2")) {}
    Expression *copy()
    {
        return new Power2(lopd->copy());
    }
    Expression *derivation() {return NULL;}
};
class Quotient: public Expression
{
public:
    Quotient(Expression *left, Expression *right): Expression("/", left, right) {}
    Expression *copy()
    {
        return new Quotient(lopd->copy(), ropd->copy());
    }
    Expression *derivation();
};
Quotient *operator/(Expression &a, Expression &b)
{
    return new Quotient(&a, &b);
}
Expression *Quotient::derivation()
{
    Expression &u = *(lopd->copy()), &v = *(ropd->copy());
    Expression &du = *(u.derivation()), &dv = *(v.derivation());
    return *(*(du * v) - * (u * dv)) / (*(new Power2(v.copy())));
}
class Minus: public Expression
{
public:
    Minus(Expression *right): Expression("_", NULL, right) {}
    Expression *copy()
    {
        return new Minus(ropd->copy());
    }
    Expression *derivation();
};
Minus *operator-(Expression &a)
{
    return new Minus(&a);
}
Expression *Minus::derivation()
{
    if (ropd->ope[0] == 'x')
        return new Const("-1");
    if (isdigit(ropd->ope[0]))
        return new Const("0");
    Expression &re = *(ropd->derivation());
    return -re;
}
class Ln: public Expression
{
public:
    Ln(Expression *right): Expression("ln", NULL, right) {}
    Expression *copy()
    {
        return new Ln(ropd->copy());
    }
    Expression *derivation();
};
Ln *ln(Expression *b)
{
    return new Ln(b);
}
Expression *Ln::derivation()
{
    Expression &v = *(ropd->copy()), &dv = *(ropd->derivation());
    return dv / v;
}
Expression *toExpression(const string &s)
{
    size_t i = 0, n = s.length();
    stack<Expression *> operands;
    stack<string> oper;
    oper.push("@");
    while (i < n)
    {
        string item;
        if (isdigit(s[i]) || s[i] == '.') //读取数值常量
        {
            while (s[i] == '.' || isdigit(s[i]))
                item += s[i++];
            operands.push(new Const(item));
            continue;
        }
        if (s[i] == 'x') //读取变量x
        {
            item += s[i++];
            operands.push(new Varible());
            continue;
        }
        if (s[i] == '(') //读取左括弧
        {
            item += s[i++];
            oper.push(item);
            continue;
        }
        item += s[i++]; //读取运算符
        if (item == "l")item += s[i++]; //是对数运算
        string t = oper.top();
        while (priority[t] > 1 && (priority[item] <= priority[t]))
        {
            oper.pop();
            Expression *l = NULL, *r = operands.top();
            operands.pop();
            if (t != "ln" && t != "_") //t是二元运算符
            {
                l = operands.top();
                operands.pop();
            }
            Expression *e;
            switch (priority[t])
            {
            case 3:
                if (t == "+") e = (*l) + (*r);
                else e = (*l) - (*r);
                break;
            case 4:
                if (t == "*")e = (*l) * (*r);
                else e = (*l) / (*r);
                break;
            case 5:
                e = ln(r);
                break;
            default:
                e = -(*r);
                break;
            }
            operands.push(e);
            t = oper.top();
        }
        if (item == ")")
            oper.pop();
        else
            oper.push(item);
    }
    return operands.top();
}
void symbleDerivation(string &s)
{
    preprocess(s);
    Expression *exp = toExpression(s);
    Expression *deriv = exp->derivation();
    delete exp;
    s = "";
    deriv->toString(s);
    delete deriv;
    fix(s);
}
int main()
{
    ifstream inputdata("inputdata.txt");
    ofstream outputdata("outputdata.txt");
    string s;
    while (inputdata >> s)
    {
        symbleDerivation(s);
        outputdata << s << endl;
        cout << s << endl;
    }
    inputdata.close();
    outputdata.close();
    return 0;
}

组合优化问题

现实中有些问题是与资源竞争相关的。这些问题往往在一组条件的限制(有限资源)下,使得利益最大或代价最小。这样的问题,通常有一组可能解,将所有可能解构成的集合称为解空间。可能解中满足约束条件的,称为合法解。每个合法解对应一个目标值 (收益或代价),目的是在解空间中找到目标值最大(小)的最优解。我们把这样的问题称为组合优化问题,有效地解决组合优化问题是计算机科学的基本任务之一。

组合问题及其回溯算法

如果在约束条件下仅要求计算出解空间中的合法解,这样的问题称为组合问题。组合问题当解空间规模不大时,将解空间组织成一棵根树,从根开始,按深度优先策略搜索合法解进而找到最优解,是一种可选的方法。这种方法由于它的深度优先策略特点,常称为回溯算法。我们先来看几个经典的组合问题及其回溯算法。

3-色问题

图的着色问题来自于地图印制:最少用几种颜色给地图中的各区域着色,使得两个相邻地区的着色不同(见图 4-1)。将地图中的区域视为一点,两个相邻区域对应的点用边连接,则得到一个无向图1G=<V, E>。地图的着色问题等价于最少用多少种颜色对 G 的顶点集 V 中的每个顶点着色,使得相邻顶点 u, v∈V,(u, v)∈E 的着色不同。将这个问题进一步简化为有m 种颜色,对图 G 的顶点着色,找出所有满足相邻顶点着色不同的着色方案。当 m=3 时,就是所谓的 3-色问题。在 3-色问题中,用 3 种颜色给图中顶点着色的有多种方案,即有多个可能解。符合约束条件——相邻顶点着色不同—的方案是问题所求的合法解。为表述简洁,设 G 的顶点集中有 n 个顶点,并表示为 V={1, 2, …, n}, 3 种颜色也表示成数字{1, 2,3}。如此,我们可以将问题的一个解表示为向量x=<x1, x2, …, xn>。每个 xk∈{1, 2, 3}表示顶点 k 的着色,1≤k≤n。由于每个 xk 都有 3 种不同的可能取值,所以 3-色问题的解空间规模为 3n。一个合法解<x1, x2, …, xn>必须满足对任一 1≤k≤n,只要 i<k 且(i, k)∈E,必有 xi≠xk。回溯方法是从顶点 k=1 开始依次考察 xk 的 3 种不同的取值,若 x k 的一个取值满足约束条件,则进而考虑 xk+1 的取值。当 xk 的 3 个取值合法性都检测过了,则考虑 xk−1 的下一种着色合法性检测。因为进行 xk 的取值检测的先决条件是 xk−1 的一个取值是符合约束条件的。因此,完成 xk 的所有取值的合法性检测后,应回到对 xk−1 的尚未完成的检测,此即所谓的回溯。当 k=n+1 时,由于<x1, x2, …, xk−1>=<x1, x 2, …, xn>是合法的,于是就得到一个完整解。将这个想法写成伪代码过程如下。

GRAPH-COLOR(G, x, k)
if k>n  判断是否为完整解
	then INSERT(solutions, <x1, x2, ..., xn>)
		return
for color←1 to 3 对当前第 k 个顶点逐一检测 3 种可能的着色
	do xk←color
		if1≤i≤k((i, k)∈ E[G] →xi≠xk) 部分合法
			then GRAPH-COLOR (G, x, k+1) 进入下一层搜索

算法 4-1 m-色问题回溯算法

这是一个递归过程,顶层调用的参数 k=1,同时将集合 solution 作为全局对象初始化为空集。由于算法是在整个解空间中搜索合法解,故运行时间为Θ (3n)。

N-后问题

国际象棋中皇后的战力是很强的。若一方皇后占据了位置(i, j) ,则棋盘上所有满足 x=i 位置(x, y) (与(i, j)处于同一行)上的对方棋子均可被皇后攻击;同样,所有满足 y=j(与(i, j)处于同一列)的以及满足|x−y|=|i−j|(与(i, j)处于同一条斜线)的位置(x, y)上的对方棋子均难逃脱被进攻的命运。N-后问题指的是在一个规模为 n×n 的棋盘上放置 n 个皇后,使得两两之间不能相互攻击(见图 4-2) ,计算出所有不同的放置格局。为解决 N-后问题,将解设置为向量 x=<x1, x2, …, xn>。其中 x k 表示在棋盘上的第 k 行放置的皇后的位置(1≤k≤n)。由于下标表示行号,故 n 个皇后不会在一行中相互攻击。为保证皇后间不会在同一列中相互攻击,<x1, x2, …,xn>必为 1,2,…,n 的一个排列。如此,问题的解空间规模为Θ(n!)。算法只需从 k=1 开始,保证每一个 k, <x1, x2, …, 图 4-2 八皇后问题的一个合法格局xk>是 1,2,…,n 的一个 k-排列,且 xk 与 xi(1≤i<k)满足|xk−xi|≠|k−i|,就是合法的。为了得到 1,2,…,n 的所有全排列,将<x1, x2, …, xn>初始化为<1, 2, …, n >。对 1≤k≤n 的 xk,逐一与<xk, xk+1, …, xn>中的元素交换得到所有<1, 2, …, n >的 k-排列,对得到的k-排列检测其合法性,若合法则进而寻求(k+1)−排列。做完 k-排列后回溯,继续进行尚未完成的(k−1)-排列。将这一思路写成伪代码过程如下。

在这里插入图片描述

N-QUEENS(x, k)if k>n	then INSERT(solutions, <x1, x2, ..., xn>)		returnfor i←k to n 对当前第 k 个分量逐一取得各种可能的值	do xi ↔ xk 交换 xi 和 xk		if | xk - xi ||k-i| for 1≤i≤k-1			then N-QUEENS(x, k+1)		xi ↔ xk 还原 xi 和 xk 准备创建下一个不同的排列

算法 4-2 N-后问题回溯算法

这也是一个递归过程。顶层调用时需将参数 x 初始化为<1, 2, …, n >k 初始化为 1。算法是在解空间中搜索合法解,故运行时间为Θ(n!)。

0-1 背包问题

一窃贼带着一个能装重量为 C 的背包,来到一个房屋。发现屋内有 n 件物品 t 1 , t 2 , . . . , t n t1, t2, ..., tn t1,t2,...,tn,重量分别为 w 1 , w 2 , . . . , w n w1, w2, ..., wn w1,w2,...,wn (见图 4-3) 。需把物品放入包内,才能把它带走。窃贼有多少种盗窃行为?此处所谓盗窃行为指的是窃贼带走哪些东西。显然,窃贼的行为受背包的承重量的约束。用向量 x = < x 1 , x 2 , . . . , x n > x=<x1, x2 , ...,xn> x=<x1,x2,...,xn>表示问题的一个解, x k ∈ 0 , 1 ( 1 ≤ k ≤ n ) x_k∈{0, 1}(1≤k≤n) xk0,1(1kn)表示第 k 件物品是装入包中(1)还是留下(0)。由此可见,问题的解空间规模为 2 n 2^n 2n。解 < x 1 , x 2 , . . . , x n > <x1, x2, ..., xn > <x1,x2,...,xn>的合法性检测条件是对1≤k≤n, ∑ i = 1 k x i w i ≤ C \sum^k_{i=1}x_iw_i\le C i=1kxiwiC。回溯算法的思想是从 k=1 开始,逐一考察 xk 的两个不同取值(0/1)是否满足约束条件。若满足约束条件 ∑ i = 1 k x i w i ≤ C \sum^k_{i=1}x_iw_i\le C i=1kxiwiC ,则进一步考察 x k + 1 x_{k+1} xk+1 的取 图 4-3 0-1 背包问题值。xk 的两个取值考察完毕回溯到对 x k−1 的尚未完成的取值考察。将这一思路写成伪代码过程如下。

KNAPSACK(x, k)
if k>n 判断是否为完整解
then INSERT(solutions, <x1, x2, …, xn>)
return
for i←0 to 1 对当前第 k 个物品逐一检测两种可能的情形
do x k x_k xk ←i
if ∑ i = 1 k x i w i ≤ C \sum^k_{i=1}x_iw_i \le C i=1kxiwiC 部分合法
then KNAPSACK(x, k+1) 进入下一层搜索

该算法也是一个递归过程。顶层调用传递给参数 k 的值应为 1,且调用前将合法解集合solutions 初始化为∅。由于算法在解空间中查找合法解,故运行时间为Θ(2n)

回溯算法框架

从上述 3 个经典问题的讨论中,可以归纳出组合问题的算法框架。首先,可以将问题的解表示成一个向量 x = < x 1 , x 2 , . . . , x n > x=<x1, x2, ..., xn> x=<x1,x2,...,xn>。一般而言, x k x_k xk 有确定的取值范围,设为 Ω k Ω_k Ωk, (1≤k≤n)。例如,在 3-色问题中 Ω k Ω_k Ωk={1,2,3}。问题的约束条件可分成对部分解合法性的检测和对完整合法解的检测两部分。设这两部分可由过程 I S − P A R T I A L ( x , k ) IS-PARTIAL(x, k) ISPARTIAL(x,k) I S − C O M P L E T ( x , k ) IS-COMPLET(x, k) ISCOMPLET(x,k)完成,则回溯算法具有如下所示的统一的形式。

BACKTACKITER(x, k)
	if IS-COMPLET(x, k)
		then INSERT(solutions, x)
			return
	for each v∈Ωk
		do xk ←v
			if IS-PARTIAL(x, k)
				then BACKTACKITER(x, k+1)

∣ Ω k ∣ = m k ( 1 ≤ k ≤ n ) |Ω_k|=m k(1≤k≤n) Ωk=mk(1kn),算法的运行时间是 O ( ∏ k − 1 n ∣ Ω k ∣ ) O(\prod_{k-1}^n| Ω_k|) O(k1nΩk)

问题 4-1 探险图

问题描述

在去往神秘世界探秘前夕,你幸运地得到了这张地图。图中展示了你想探索的整个区域,包含若干个国家或地区,这些地区有着复杂的边界。地图描绘得还算清楚,但是只用了一种棕褐色墨水,所以很难一下子就看清楚哪块区域从属于哪个国家或地区。这种状况可能会给你的探索带来危险。你决定在出发之前重新对地图进行着色。“有备无患…”,你自言自语地嘟囔着。每个国家有着若干条边界,每条边界都构成一个多边形。任意两国边界或许从不相交,或许有共同部分。为便于查看,属于同一个国家的区域应染同一种颜色。可以对多个国家染同一色,但必须不会发生混淆。也就是说,相邻(有部分相同边界)的两个国家必须着不同的颜色。写一个程序,计算为地图着色需要的最少颜色数。

输入

输入包含若干个测试案例,每个案例描述一幅地图。每幅地图开头一行仅含一个表示地图中区域个数的整数 n。接着是描述每个区域封闭边界的若干行数据,格式如下:

String
x1 y1
x2 y2

xm ym
-1

其中首行中的“String”表示该区域所属国家名。国家名长度介于 2~20 个字符之间。若一个国家拥有多个区域,则每个区域前都标识了该国家的名称。接着的每一行是表示该区域多边形边界顶点坐标的两个整数 x 和 y(0≤x, y≤1000)。相邻两个顶点表示多边形的一条边,最后一个顶点与第一个顶点表示一条边。一行仅含−1,为区域描述数据的结束标志。一个区域边界的顶点数不超过 100。每个区域边界都是简单多边形,即边界上的边无交叉。另外任意两个区域都没有相交面积,地图中的国家数不超过 10。区域数 n=0 为输入文件结束标志。

输出

对每一个测试案例输出一行数据,其中包含按要求对地图着色所需的最少颜色数

输入样例

6
Blizid
0 0
60 0
60 60
0 60
0 50
50 50
50 10
0 10
-1
Blizid
0 10
10 10
10 50
0 50
-1
Windom
10 10
50 10
40 20
20 20
20 40
10 50
-1
Accent
50 10
50 50
35 50
35 25
-1
Pilot
35 25
35 50
10 50
-1
Blizid
20 20
40 20
20 40
-1
4
A1234567890123456789
0 0
0 100
100 100
100 0
-1
B1234567890123456789
100 100
100 200
200 200
200 100
-1
C1234567890123456789
0 100
100 100
100 200
0 200
-1
D123456789012345678
100 0
100 100
200 100
200 0
-1
0

输出样例

4
2

解题思路
(1)数据输入与输出
根据输入文件格式,对每个测试案例,首先从中读取区域个数n。创建表示地图的集合map,其中的元素为二元组<country, territory>。country 就是表示国家名的串,而 territory 存储对应国家的边界上的所有边的集合。对每个区域,先读取所属国家名称(一行)name,若map 中不存在名为 name 的国家,则在 map 中加入元素<name, ∅>,否则取该元素。然后从输入文件中依次读取该区域边界的每一个顶点(x, y),相邻顶点构成的边加入 map 中该元素的 territory,别忘了将最后顶点与第一个顶点构成的边也加入其中。读到 x=−1 则意味着区域数据读取完毕。对存储在 map 中的案例数据,计算能使相邻区域不同色的地图着色方案的最小颜色数,将计算所得结果作为一行写入输出文件。读到案例的区域数 n=0,意味着输入文件结束。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 n
while n>0
	do 创建集合 map←∅
		for i←1 to n
			do 从 inputdata 中读取一行 s
				if map 中不存在 country 为 s 的元素
					then INSERT(map, <s,>)
				(country, territory)FIND(map, s)
				从 inputdata 中读取一行 s
				从 s 中解析出(x, y)
				(x0, y0)(x1 , y1)(x, y)
				从 inputdata 中读取一行 s
				while s≠"−1"
					do 从 s 中解析出(x, y)
						INSERT(territory, ((x1 , y1), (x, y)))
						(x1, y1 )(x, y)
						从 inputdata 中读取一行 s
					INSERT(territory, ((x1, y1), (x0, y0)))
			result←COLOR-THE-MAP(map)
			将 result 作为一行写入 outputdata
			从 inputdatat 中读取 n
关闭 inputdata
关闭 outputdata

其中,第 21 行调用计算地图 map 着色所用最少颜料数的过程 COLOR-THE-MAP(map),是解决一个案例的关键

(2)处理一个案例的算法过程

​ 根据本章第 1 节中讨论过的图的 m-色问题,我们知道本问题的一个案例可以根据数据集合 map 构造一个无向图 G<V, E>,其中顶点集 V 为地图中的各个国家,对于两个国家 u,v 若有部分公共边界,则(u, v)∈ E,如图 4-4 所示。若 G 是一个平凡图 2,则仅用 1 种颜色即可完成地图着色。否则令 m 从 2 开始,调用算法 4-1 GRAPH-COLOR,若 solution 为∅,m自增 1,再次调用 GRAPH-COLOR,直至 solution非空。返回 m 即为所求

在这里插入图片描述

COLOR-THE-MAP(map)
G←MAP-TO-GRAPH(map)
 if G 是一个平凡图
	then return 1
m←2, solution←∅
GRAPH-COLOR(G, x, 1)
while solution=do m←m+1
		GRAPH-COLOR(G, x, 1)
return m

算法 4-5 解决“探险图”问题一个案例的算法过程

​ 其中,第 1 行调用的过程 MAP-TO-GRAPH(map)是将地图数据 map 转换成表示无向图的矩阵 G。

MAP-TO-GRAPH(map)
	n←length[map]
G←(0)n×n
将 map 中 n 个国家编号 1~n
for i←1 to n-1
	do for j←i+1 to n
		do if ADJACENT(territory[map[i]], territory[map[j]])
			then G[i, j]←G[j, i]1
return G

算法 4-6 将地图数据 map 转换成无向图矩阵表示的算法过程

其中,第 6 行调用的过程 ADJACENT(territory[map[i]], territory[map[j]])用于检测两个国家的边界是否存在部分相交。由于每个国家的边界是由若干条直线段构成,即每个国家country 的边界 territory[country]中的一个元素为边 s=(p, q),而 p,q 为由形如坐标(x, y)表示的两个点。 为检测两个国家 country1 和 country2 的边界 territory[country1]与 territory[country2]有无部分相交可以描述为如下过程。

ADJACENT(territory[country1], territory[country2])
	 for each s1∈ territory[country1]
		do for each s2∈ territory[country2]
			do if OVERLAP(s1, s2)
				then return true
return false

算法 4-7 检测两个国家边界有无部分重合的算法过程

其中第 3 行调用过程 OVERLAP(s1, s2)检测两条线段是否有部分重合。设 s1=(a1, b1),s2=(a2, b2)。要判断平面上两条不同的线段 s1 与 s2 是否有部分重合,需要考虑如下两个条件:
1 s1 是否与 s2 平行。
2 在1为真的前提下,若 s2 的一个端点,不妨记为 a 在以 s1 为对角线的矩形框内。
3 在1、2均为真的前提下,线段(a1, a)与(a, b1)平行。如图 4-5 所示。
据此,过程 OVERELAP 的伪代码描述如下。

OVERLAP(s1 , s2)
if s1=s2
	then return true
if s1 平行于 s2
	then if s2 的一端 a 位于 s 1 =(a 1 , b1 )为对角线的矩形框内
		then if (a1, a)(p, a1)平行
			then return true
return false

算法 4-8 检测两条线段是否部分重合的算法过程

在这里插入图片描述

显然,这是一个常数时间的操作。若设各国家边界上的边数平均为 p,则算法 4-7 的运行时间为Θ (p2)。于是,算法 4-6 中第 6 行是内嵌于第 4~7 行的两重循环内,其运行时间为Θ (n2p2)。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <hash_map>
#include <vector>
using namespace std;
struct Segment
{
    pair<int, int> a, b;
    Segment(pair<int, int> a1, pair<int, int> b1): a(a1), b(b1) {}
};
bool operator ==(pair<int, int> a, pair<int, int> b)
{
    return (a.first == b.first && a.second == b.second);
}
bool operator ==(Segment &s1, Segment &s2)
{
    return (s1.a == s2.a && s1.b == s2.b) || (s1.a == s2.b && s1.b == s2.a);
}
bool overlap(Segment &s1, Segment s2)
{
    if (s1 == s2)
        return true;
    int v1 = abs(s1.a.first - s1.b.first) * abs(s2.a.second - s2.b.second),
        v2 = abs(s1.a.second - s1.b.second) * abs(s2.a.first - s2.b.first),
        v3 = 1, v4 = 2;
    if (v1 == v2)
    {
        int x1 = min(s1.a.first, s1.b.first), x2 = max(s1.a.first, s1.b.first),
            y1 = min(s1.a.second, s1.b.second), y2 = max(s1.a.second, s1.b.second);
        if (s2.a != s1.a && s2.a != s1.b)
        {
            if (x1 <= s2.a.first && s2.a.first <= x2)
            {
                if (y1 <= s2.a.second && s2.a.second <= y2)
                {
                    v3 = abs(s1.a.first - s2.a.first) * abs(s2.a.second - s1.b.second);
                    v4 = abs(s2.a.first - s1.b.first) * abs(s1.a.second - s2.a.second);
                }
            }
        }
        else
        {
            if (x1 <= s2.b.first && s2.b.first <= x2)
            {
                if (y1 <= s2.b.second && s2.b.second <= y2) //s2.b在s1为对角线的矩形框内
                {
                    v3 = abs(s1.a.first - s2.b.first) * abs(s2.b.second - s1.b.second);
                    v4 = abs(s2.b.first - s1.b.first) * abs(s1.a.second - s2.b.second);
                }
            }
        }
        if (v3 == v4)
            return true;
        x1 = min(s2.a.first, s2.b.first), x2 = max(s2.a.first, s2.b.first);
        y1 = min(s2.a.second, s2.b.second), y2 = max(s2.a.second, s2.b.second);
        if (s1.a != s2.a && s1.a != s2.b)
        {
            if (x1 <= s1.a.first && s1.a.first <= x2)
            {
                if (y1 <= s1.a.second && s1.a.second <= y2)
                {
                    v3 = abs(s2.a.first - s1.a.first) * abs(s1.a.second - s2.b.second);
                    v4 = abs(s1.a.first - s2.b.first) * abs(s2.a.second - s1.a.second);
                }
            }
        }
        else
        {
            if (x1 <= s1.b.first && s1.b.first <= x2)
            {
                if (y1 <= s1.b.second && s1.b.second <= y2) //s2.b在s1为对角线的矩形框内
                {
                    v3 = abs(s2.a.first - s1.b.first) * abs(s1.b.second - s2.b.second);
                    v4 = abs(s1.b.first - s2.b.first) * abs(s2.a.second - s1.b.second);
                }
            }
        }
        if (v3 == v4)
            return true;
    }
    return false;
}
bool adjacent(vector<Segment> &a, vector<Segment> &b)
{
    int n = a.size(), m = b.size();
    for (int i = 0; i < n; i++)
    {
        Segment s1 = a[i];
        for (int j = 0; j < m; j++)
        {
            Segment s2 = b[j];
            if (overlap(s1, s2))
                return true;
        }
    }
    return false;
}
class Map
{
private:
    vector<vector<int>> G, solution;
    bool trivial;
    int n;
    void graphColor(int k, vector<int> &x)
    {
        if (k >= n)
        {
            solution.push_back(vector<int>(x));
            return;
        }
        for (int color = 1; color <= m; color++)
        {
            x[k] = color;
            int i = 0;
            while (i < k)
            {
                if (G[i][k] == 1 && x[i] == x[k])
                    break;
                i++;
            }
            if (i >= k)
                graphColor(k + 1, x);
        }
    }
public:
    int m;
    Map(hash_map<string, vector<Segment>> map)
    {
        n = map.size();
        vector<string> contry;
        for (hash_map<string, vector<Segment>>::iterator i = map.begin(); i != map.end(); i++)
        {
            contry.push_back(i->first);
            G.push_back(vector<int>(n, 0));
        }
        trivial = true;
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++)
                if (adjacent(map[contry[i]], map[contry[j]]))
                {
                    G[i][j] = 1; G[j][i] = 1;
                    trivial = false;
                }
        m = 1;
    }
    void colorTheMap()
    {
        if (trivial)
            return;
        int n = G.size();
        vector<int> x(n, 0);
        m++;
        graphColor(0, x);
        while (solution.empty())
        {
            m++;
            fill(x.begin(), x.end(), 0);
            graphColor(0, x);
        }
    }
};
int main()
{
    ifstream inputdata("inputdata.txt");
    ofstream outputdata("outputdata.txt");
    string s;
    int n;
    inputdata >> n; getline(inputdata, s);
    while (n > 0)
    {
        hash_map<string, vector<Segment>> map;
        for (int i = 0; i < n; i++)
        {
            string contry;
            getline(inputdata, contry);
            int x, y;
            inputdata >> x >> y; getline(inputdata, s);
            pair<int, int> p0 = make_pair(x, y), p1 = make_pair(x, y);
            getline(inputdata, s);
            while (s != "-1")
            {
                istringstream strstr(s);
                strstr >> x >> y;
                pair<int, int> p2 = make_pair(x, y);
                map[contry].push_back(Segment(p1, p2));
                p1 = pair<int, int>(p2);
                getline(inputdata, s);
            }
            map[contry].push_back(Segment(p1, p0));
        }
        Map aMap = Map(map);
        aMap.colorTheMap();
        outputdata << aMap.m << endl;
        cout << aMap.m << endl;;
        inputdata >> n; getline(inputdata, s);
    }
    inputdata.close();
    outputdata.close();
    return 0;
}

(代码看不懂阿)

问题 4-2 Jill 的骑行路径

问题描述

每年,Jill 都要在两个村庄之间做一次骑行旅游。两个村庄之间有多条路径,但 Jill 的体力有限,只适合于在体力允许的里程范围内的线路。给定旅游地图,上面标有各村镇及连接这些村镇的道路(同时还标有这些道路的里程)。Jill 希望罗列出所有适合她体力的路线,以供选择。你的任务是写一段程序,按里程升序列出所有这样的路径。任意两个村庄之间至多有一条道路连接,道路是双向的,且里程是正值。不存在从一个村庄直接回到原地的道路。Jill 只考虑去程,不考虑回程。Jill 对任何一个村庄都不想经过两次。

Jill 能行进的最远里程不超过 9999。
输入
输入文件包含若干个测试案例。每个案例包含一幅地图,起点和终点村庄以及 Jill 能行进的最大里程。
每一个测试案例数据由若干个被空格或换行符隔开的整数组成。具体格式解释如下:
NV——地图中的村庄数,这个数据至多为 20。
NR——地图中连接两个不同村庄的道路数。
NR 个三元组 C1 ,C2 和 DIST,分别表示一条道路连接的两个村庄及其长度。
SV, DV——分别表示起点村庄和终点村庄;所有 NV 个村庄用 1~NV 编号。
MAXDIST——表示 Jill 所能行进的最大里程(单程)。
NR=−1 是输入数据结束标志。
输出
对每一个测试案例,第一行输出 Case 案例号(1,2,…)。然后一行输出一条 Jill 可行的路径。路径以长度开头,后跟从起点开始路径所经过的村庄编号序列,终点为最后一个数。各条路径按长度的升序逐一输出。两条以上路径具有相同里程,则按路径中顶点序列的字典顺序排列。请严格按照输入样例和输出样例的格式输入输出数据。若案例不存在满足条件的路径则输出一行“ NO ACCEPTABLE TOURS”。
案例之间输出一空行。

输入样例
4 5
1 2 2
1 3 3
1 4 1
2 3 2
3 4 4
1 3
4
4 5
1 2 2
1 3 3
1 4 1
2 3 2
3 4 4
1 4
10
5 7
1 2 2
1 4 5
2 3 1
2 4 2
2 5 3
3 4 3
3 5 2
1 3
8
−1

输出样例
Case 1:
3: 1 3
4: 1 2 3
Case 2:
1: 1 4
7: 1 3 4
8: 1 2 3 4
Case 3:
3: 1 2 3
7: 1 2 4 3
7: 1 2 5 3
8: 1 4 2 3
8: 1 4 3

解题思路

(1)数据输入与输出
按输入文件格式,依次读取其中的每个测试案例的数据。在测试案例的第 1 行读取表示村庄个数与连接村庄的道路数的 NV 及 NR。设置矩阵 G=(0)NV×NV,在随后的 NR 行中,每行读取三元组 C1,C2 和 DIST,并置 G[C1, C2]及 G[C2, C1]为 DIST。接着从输入文件中读取起点村庄和终点村庄 SV 和 DV。最后读取里程数极限 MAXDIST。对 G、SV、DV 和 MAXDIST计算 Jill 能骑行的路径列表,并按路径里程的升序,逐行写入输出文件。若没有合适于 Jill体力的路径,则输出一行“NO ACCEPTABLE TOURS”。循环往复直至读取到 NV=−1,意味着输入文件结束。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 NV
number←0
while NV>0
	do number←number+1
	创建 G[1..NV, 1..NV]并初始化为零矩阵
	从 inputdata 中读取 NR
	for i←1 to NR
		do 从 inputdata 中读取 C1,C2,DIST
			G[C1, C2]←G[C2, C1]←DIST
从 inputdata 中读取 SV, DV, MAXDIST
path←{SV}
result←JILL-TOUR-PATHS(path, 2)
SORT(result)"Case number"作为一行写入 outputdata
if result=∅
	then 将" NO ACCEPTABLE TOURS"作为一行写入 outputdata
	else for each r∈result
		do 将 r 作为一行写入 outputdata
	在 outputdata 中写入一个空行
	从 inputdata 中读取 NV
关闭 inputdata
关闭 outputdata

其中,第 12 行调用计算 Jill 的骑行路径列表的过程 JILL-TOUR-PATHS(path, 2),是解决一个案例的关键。

(2)处理一个案例的算法过程

对于一个案例的数据 G, SV, DV, MAXDIST,为计算出 Jill 的骑行线路,可以设置一个路径向量 path=(SV, v2, …, v k),从与 SV 相邻的 v2 开始依次探索,并跟踪边(SV, v2) 、 (v2 , v3),…,(vk−1, vk)的长度之和,即路径里程。若 vk≠DV,且若长度未达到 MAXDIST 则进一步搜索,将路径 path 扩张到(SV, …, vk, vk+1),否则回溯到(SV, …, vk−1)。将满足条件的完整合法解path=(SV, v2, …, DV)及其长度加入 solutions 中。搜索完所有可能解, solutions 即为所求。对算法 4-4 所示的回溯算法框架稍加修改得到:

JILL-TOUR-PATHS(path, k)
if path[k-1]=DV
then 将 length 插在 path 首部
	INSERT(solutions, path)
	return
for v←1 to NV
	do if G[path[k-1], v]0 and v∉path
		then if length+ G[path[k-1], v]≤MAXDIST
			then xk ←v
				length←length+ G[path[k-1], v]
				JILL-TOUR-PATHS(path, k+1)
				从 path 中删掉 v
				length←length- G[path[k-1], v]

算法 4-9 解决“Jill 的骑行路线”问题一个案例的算法过程

其中,第 2 行将 length 插在 path 的首部,以便于输出前按该元素的升序排序。算法的运行时间为 Θ ( N V N V ) Θ (NV^{NV}) Θ(NVNV)

#include <iostream>
#include <fstream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;
class Map
{
	vector<vector<int>> &g;
	int sv,dv,maxdist,length;
	vector<int> path;
public:
	vector<vector<int>> solution;
	Map<vector<vector<int>>&G,int SV,int DV, int MAXDIST>:g(G),sv(SV - 1),dv(DV - 1),maxdist(MAXDIST)
	{
		path.push_back(sv);
		length = 0;
		solution = vector<vector<int>>();
	}
	void jillsTourPaths(int k)
	{
		if(path[k - 1] == dv)
		{
			solution.push_back(vector<int>(path));
			solution[solution.size() - 1].insert(solution[solution.size() - 1].begin(),length);
			return;
		}
		int nv = g.size();
		for(int v = 0;v < nv;v++)
		{
			if(g[path[k - 1]][v] > 0 && find(path.begin(), path.end(),v) == path.end())
			{
				if(length + g[path[k - 1]][v] <= maxdist)
				{
					path.push_back(v);
					length += g[path[k - 1]][v];
					jillsTourPaths(k + 1);
					length -= g[path[k - 1]][v];
					path.pop_back();
				}
			}
		}
	}
	void fix()
	{
		int n = solution.size();
		for(int i = 0;i < n; i++)
		{
			int m = solution[i].size();
			for(int j = 1;j < m;j++)
				solution[i][j]++;
		}
		sort(solution.begin(),solution.end(),less<vector<int>>());
	}
};

int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	int NV,number = 0;
	inputdata >>NV;
	while(NV > 0)
	{
		number++;
		vector<vector<int>> G(NV, vector<int>(NV,0));
		int NR;
		inputdata >> NR;
		for(int i = 0;i < NR; i++)
		{
			int C1,C2,DIST;
			inputdata>>C1>>C2>>DIST;
			G[C1 - 1][C2 - 1] = DIST;
			G[C2 - 1][C1 - 1] = DIST;
		}
		int SV,DV,MAXDIST;
		inputdata>>SV>>DV>>MAXDIST;
		Map aMap(G,SV,DV,MAXDIST);
		aMap.jillsTourPaths(1);
		aMap.fix();
		outputdata << "Case "<<number<<" :\n";
		cout<<"Case "<<number<<" :\n";
		for(int i = 0;i<aMap.solution.size();i++)
		{
			outputdata<<aMap.solution[i][0]<<": ";
			copy(aMap.solution[i].begin() + 1,aMap.solution[i].end(),ostream_iterator<int>(outputdata, " "));
			outputdata<<endl;
			cout<<aMap.solution[i][0] <<": ";
			copy(aMap.solution[i].begin() + 1,aMap.solution[i].end(),ostream_iterator<int>(cout," "))
			cout<<endl;
		}
		outputdata << endl;
		cout<<endl;
		inputdata>>NV;
	}
	inputdata.close();
	outputdata.close();
	return 0;
}

排列树问题

特殊地,若问题的解向量 x = < x 1 , x 2 , . . . , x n > x=<x_1, x_2, ..., x_n> x=<x1,x2,...,xn>必须是一组已知 n 个值的排列,如 N-后问题,称其为排列树问题。对排列树问题,回溯算法不必像上述算法那样对每一个 x k x_k xk 取遍所有 n个可能的值,而有如下更有效的形式

PERMUTATION-TREE(x, k)
	if IS-COMPLET(x, k)
		then INSERT(solutions, x)
			return
for i←k to n
	do xi ↔ xk 交换 xi 和 xk
		if IS-PARTIAL(x, k)
			then PERMUTATION-TREE(x, k+1)
xi ↔ xk 还原 xi 和 xk 准备创建下一个不同的排列

排列树算法框架

算法的运行时间为Θ (n!)。

问题 4-3 八元拼图

问题描述

用数字 1~8 填充下列的 8 个圆圈,每个数只用 1 次。两个相连的圆圈不能填写连续数字。

共有 17 对相连的圆圈(见图 4-6):

在这里插入图片描述

A-B, A-C, A-D
B-C, B-E, B-F
C-D, C-E, C-F, C-G
D-F, D-G
E-F, E-H
F-G, F-H
G-H

在圆圈 G 和 D 中填充 1 和 2(或填充 2 和 1)是不合法的。因为 G和 D 是相连的,而 1 和 2 是连续的两个数字。然而,在圆圈 A 中填 8且在 B 中填 1 是合法的,因为 8 和 1 不是连续数字。本题中,已有若干个圆圈已填充,你的任务是填充其余各圆圈,来得到一个合法解(如果存在)。

在这里插入图片描述

输入

输入的第一行仅含一个整数 T (1 ≤T≤ 10),表示测试案例个数。每个测试案例有一行数据,这行数据由 8 个 0~8 的数字组成,数字之间用空格隔开,对应 A~H 八个圆圈。0表示是个空的圆圈。

输出

对每一个测试案例,打印出案例编号,并以与输入一样的格式打印出个圆圈中的数字。如果该案例无解,打印“No answer”。若存在多个合法解,打印“Not unique”。

输入样例

3
7 3 1 4 5 8 0 0
7 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0

输出样例

Case 1: 7 3 1 4 5 8 6 2
Case 2: Not unique
Case 3: No answer

解题思路

(1)数据输入与输出

根据输入文件格式,首先从中读取测试案例数 T,然后依次读取 T 个案例数据,每个案例占一行,包含 8 个整数。将这 8 个数组织成一个数组 a。对 a 计算填写八角棋盘的合法方案,若结果中只有一个方案,将计算所得结果作为一行写入输出文件。若有多个可行方案,则将“Not unique”作为一行写入输出文件。若案例没有合法解则向输出文件写入一行“Noanswer”。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 T
for t←1 to T
	do 创建数组 a[1..8]
		for i←1 to 8
			do 从 inputdata 中读取 a[i]
		result← EIGHT-PUZZLE(a)
		if result≠∅
			then if result中仅有 1 个元素
				then 将 result 中的元素作为一行写入 outputdata
关闭 inputdata
关闭 outputdata

其中,第 8 行调用计算合法填充方案的过程 EIGHT-PUZZLE(a)是解决一个案例的关键。
(2)处理一个案例的算法过程8 个圆圈及圆圈之间的连接关系对应一个无向图(见图 4-6) ,将 A~H 编号 1~8 则该无向图可表示为 8×8 矩阵 A,有
A = ( a i j ) 8 x 8 = ( 0 1 1 1 0 0 0 0 1 0 1 0 1 1 0 0 1 1 0 1 1 1 1 0 1 0 1 0 0 0 1 0 0 1 1 0 0 1 0 1 0 1 1 1 1 0 1 1 0 0 1 1 0 1 0 1 0 0 0 0 1 1 1 0 ) A=(a_{ij})_{8x8} = \left(\begin{array}{l}0&1&1&1&0&0&0&0 \\ 1&0&1&0&1&1&0&0 \\1&1&0&1&1&1&1&0 \\1&0&1&0&0&0&1&0 \\0&1&1&0&0&1&0&1 \\0&1&1&1&1&0&1&1 \\0&0&1&1&0&1&0&1\\ 0&0&0&0&1&1&1&0\end{array}\right) A=(aij)8x8=0111000010101100110111101010011001100101011010110011010100001110
式中,aij=1 表示顶点 i 和 j 之间相互连接(同时必有 aji=1 ),而 aij=0 表示 i 和 j 之间没有连接(同时必有 aji=0)。式(4-1)称为该图的邻接矩阵。

任一案例的合法解可表为一个向量 x = < x 1 , x 2 , x 3 , x 4 , x 5 , x 6 , x 7 , x 8 > x =<x_1, x_2, x_3, x_4, x_5, x_6, x_7, x_8> x=<x1,x2,x3,x4,x5,x6,x7,x8>。各分量构成 1~8 的一个排列。如果问题是计算这个游戏的所有合法解,则很容易理解这是一个排列树问题。解向量 x = < x 1 , x 2 , x 3 , x 4 , x 5 , x 6 , x 7 , x 8 > x=<x_1, x_2, x_3, x_4, x_5, x_6 , x_7, x_8> x=<x1,x2,x3,x4,x5,x6,x7,x8>是{1,2, 3,4, 5, 6, 7, 8}的一个排列。满足条件:对任意的两个分量 x i x_i xi x j x_j xj(i≠j) ,若 x i , x j x_i,x_j xi,xj 所在的圆圈相连,有 ∣ x i − x j ∣ ≠ 1 |x_i−x_j|≠1 xixj=1。则该解向量是合法的。将 x = < x 1 , x 2 , x 3 , x 4 , x 5 , x 6 , x 7 , x 8 > x=<x_1, x_2, x_3, x_4, x_5, x_6, x_7, x_8> x=<x1,x2,x3,x4,x5,x6,x7,x8>初始化为 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 {1 ,2,3,4,5,6,7,8} 1,2,3,4,5,6,7,8,合法解个数计数器 count初始化为 0,套用解决排列树问题的回溯算法 4-10,可以写出下列伪代码过程。

PERMUTATION-TREE (x, k)
if k>8
	then INSERT(solution, x)
		return
for i←k to n
	do xi ↔ xk 交换 xi 和 xk
		for j←1 to k-1
			do if A[j, k]=1 and | xj-xk |=1
				then break this loop
		if j=k
			then PERMUTATION-TREE (x, k+1)
xi ↔ xk 还原 xi 和 xk 准备创建下一个不同的排列

算法中,每得到一个合法解,计数器 count 自增 1,返回上一层(第 1~3 行)。搜索过程中,对当前顶点 k,通过 x[k]与其后的各元素值交换形成所有的可能的排列(第 4~11 行),检测 x[k]当前填充值是否满足合法条件,即顶点 k 与 1,…, k−1 中任意之一 j 相连均应有| xj−xk|≠1。这由第 6~8 行的 for 循环完成检测,在此循环中只要发现顶点 j(1≤j<k)与 k 相连且所填数字| xj−xk |=1,就中断检测(j<k)。若从始至终均未检测到此情形,循环结束时必有 j≥k。第 9~10 行由此判断是否进一步探索顶点 k+1。然而,本问题中,对一个案例而言,解向量有部分分量的值是固定的。所以,算法应当对非固定值的分量构成的子向量进行排列树的回溯探索。例如,输入样例中的案例 1,我们要对由集合{2, 6}构成的子向量<x7, x8>做排列树探索;对案例 2 中集合{1, 2, 3, 4, 5, 6,8}构成的子向量< x2, x3, x4, x5, x6, x7, x8>做排列树探索;而对案例 3 的集合{2,3,4,5,6,7, 8}构成的子向量< x 2, x3, x4, x5, x6, x7, x8>做排列树探索。一般地,设< x1, x2, x3, x4, x5, x6, x7,x8>中子向量 x i 1 , xi 2 ,…,x in 为确定排列。我们可以将该子向量下标存储到数组 index 中,即index[1…n]=<i1, i 2,…, in>。向量 x 中其他元素则是固定不变的。于是问题改变成对 index 的排列树搜索。

ANOTHER-EIGHT-PUZZLE(a)
	创建式 4-1 定义的 8 阶方阵 A
	b←{1, 2, ..., 8}, index←∅
	for i←1 to 8
		do if a[i]=0
			then INSERT(index, i)
			else b←b-{a[i]}
	solutions←∅, n←length[index]
	x←a
	for i←1 to n
		do x[index[i]]←b[i]
	PERMUTATION-TREE (x, 1)
	return solution

算法 4-12 解决“八元拼图”一个案例的算法

其中第 11 行调用的排列树搜索过程 PERMUTATION-TREE (x, k)需要对算法 4-11 做如下修改:

PERMUTATION-TREE (x, k)
	if k>8
		then INSERT(solution, x)
		return
	if k∈index x[k]为为确定元素位置
		then k1←k 在 index 中的下标, n←length[index]
			for i←k1 to n
				do xindex[i] ↔ xk
			for j←1 to k−1
				do if A[j, k]=1 and | xi-xk |=1
					then break this loop
				   if j=k
					then PERMUTATION-TREE (x, k+1)
				   xindex[i] ↔ xk
else for j←1 to k-1
	do if A[j, k]=1 and | xj-xk |=1
        then break this loop
	if j=k
		then PERMUTATION-TREE (x, k+1)

算法 4-13 < x1, x2, x3, x4, x5, x6, x7 , x8>的子序列 xi 1 , x i2 ,…,xi n 进行排列的算法过程

如前所述,排列操作是对由 index 所决定的未确定位置的元素子集进行的。所以需要根据 k 是否为确定元素(第 4 行的检测)位置而区别处理。第 5~13 行的操作对应 k 为未确定元素位置。它需要在 index 中进行排列。第 6~13 行的 for 循环就是完成这一任务的,它与算法 4-9 中第 4~11 行的操作意义相近。第 14~18 行的操作是对 k 为一确定元素位置添加的操作。这是因为在 x[k]之前可能加入了新的元素,尚未检测这样的新元素与 x[k]是否合法。其实,这部分操作前面第 8~12 行的操作是一样的,就是检测 x[k]与 x[1…k−1]是否可以构成部分合法解。若是,则进一步探索。由于算法 4-10 的运行时间是Θ (n!),所以算法 4-11 的运行时间也是Θ (n!)。进而,算法4-13 的运行时间以及调用算法 4-13 的算法 4-12 的运行时间也是Θ (n!)。其中,n=8。

#include <fstream>
#include <vector>
#include <iostream>
#include <string>
#include <iterator>
#include <bitset>
#include <algorithm>
using namespace std;
class Puzzle
{
	vector<bitset<8>> A;
	vector<int> index;
	vector<int> x;
	vector<vector<int>> solution;
public:
	Puzzle(vector<int>&a)
	{
		A = vector<bitset<8>>();
        A.push_back(bitset<8>("00001110"));
        A.push_back(bitset<8>("00110101"));
        A.push_back(bitset<8>("01111011"));
        A.push_back(bitset<8>("01100101"));
        A.push_back(bitset<8>("10100110"));
        A.push_back(bitset<8>("11011110"));
        A.push_back(bitset<8>("10101100"));
        A.push_back(bitset<8>("01110000"));
		vector<int> b;
		for(int i = 0;i<8;i++)
			b.push_back(i + 1);
		x = vector<int>(a);
		for(int i = 0;i < 8;i++)
		{
			if(a[i] != 0)
			{
				vector<int>::iterator p = find(b.begin(),b.end(),a[i]);
				b.erase(p);
			}
			else
				index.push_back(i);
		}
		x = vector<int>(a);
		for(int i = 0;i < index.size();i++)
			x[index[i]] = b[i];
	}
private:
	bool isPartial(int k)
	{
		int j = 0;
		for(j = 0;j < k;j++)
			if(A[j][k] && abs(x[j] - x[k]) == 1)
				break;
		if(j == k)
			return true;
		return false;
	}
public:
	void permutation(int k)
	{
		if(k >= 8)
		{
			solution.push_back(vector<int>(x));
			return;
		}
		vector<int>::iterator k1 = find(index.begin(),index.end(),k);
		if(k1 != index.end())
		{
			for(int i = distance(index.begin(),k1);i != index.size();i++)
			{
				if(index[i] != k)
					swap(x[index[i]],x[k]);
				if(isPartial(k))
					permutation(k + 1);
				if(index[i] != k)
					swap(x[index[i]],x[k]);
			}
		}
		else if(isPartial(k))
			permutation(k + 1);
	}
	string toString()
	{
		string s;
		int n = solution.size();
		switch(n)
		{
			case 0:s = "No answer\n";break;
			case 1:s = "Not unique\n";break;
			default:s = "               ";
				for(int i = 0;i < 8;i++)
					s[2 * i] = '0' + solution[0][i];
				s[15] = '\n';
		}
		return s;
	}
};

string anotherEightPuzzle(vector<int> &a)
{
	Puzzle p(a);
	p.permutation(0);
	return p.toString();
}

int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	int T;
	inputdata>>T;
	for(int t = 1;t <= T;t++)
	{
		vector<int> a(8);
		for(int i = 0; i<8;i++)
			inputdata>>a[i];
		string result = anotherEightPuzzle(a);
		outputdata<<"Case "<<t<<":	"<<result;
		cout<<"Case "<<t<<":	"<<result;
	}
	inputdata.close();
	outputdata.close();
	return 0;
}

问题 4-4 一步致胜

问题描述

4×4 的一字棋的棋盘有 4 行(从 0 到 3 编号) 4 列(也是从 0到 3 编号)。两个玩家 x 和 o 轮流落子。每一局都是从 x 开始。谁的棋子先占满一行或一列或主对角线或副对角线,谁就赢得游戏。若棋盘布满了棋子,但没有玩家占据一行、一列或一对角线,算平局。假定轮到 x 下子。如果 x 可以在落子后保证在后面的棋局中无论 o 如何落子, x 都将赢,则 x 可称为必赢。这并不意味着 x 恰在下一步就赢,虽然这也是可能的。x 必赢的意思是 x 有一个策略,无论 o 如何行进,最终都是 x 赢。你的任务是写一个程序,对给定的一个残局,若轮到 x 下子,确定 x 是否必赢。可以假定,每个玩家至少已经走过两步,任一玩家都尚未赢得游戏,棋盘也没有布满棋子。

输入

输入含有若干个测试案例。以一行仅含一个美元符号的数据表示文件结束。每个测试案例由一行仅含问号的数据开始,后跟 4 行表示棋局的数据,格式如下列的输入案例所示。表示棋局的数据所用的字符为句点.(表示空格子) ,小写字母 x 及小写字母 o。对每一个测试案例,输出一行表示 x 必赢的第一步落子位置(行号,列号)的数据。若不能必赢,则输出一行“#####”。输出格式如下列的输出样例所示。

输出

对每一个问题,输出的是必赢方案的第一步落子位置,而不是赢得胜利的步数。按(0,0) , (0, 1), (0, 2) , ( 0, 3) , (1, 0 ), (1, 1) ,…, (3, 2) , (3, 3)的顺序检测必赢,并输出必赢的第一步位置。若有多个必赢策略,输出按此顺序最先发现的必赢策略的第一步位置。

输入样例

?

.xo.
.ox.

?
o…
.ox.
.xxx
xooo
$

输出样例

(0,3)

解题思路(1)

数据输入与输出根据输入文件格式,依次从中读取各测试案例。每个案例的第 1 行为起始标志 flag= “?” 。接着是 4 行描述棋盘格局的字符串,将棋盘数据组织成字符串数组 board[1…4]。对案例数据board 计算是否存在 x 方必赢的首步,若有则将首步位置作为一行写入输出文件,否则输出一行“#####”。循环往复,直至从输入文件中读到 flag=“$”。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 flag
while flag="?"
	do 创建数组 board[1..4]
		从 inputdata 中读取一行到 board[i]
		FIND-WINNING-MOVE(board)
	if x-force-win=TRUE
		then 将 win-move 作为一行写入 outputdata
		else"#####"作为一行写入 outputdata
	从 inputdata 中读取 flag
关闭 inputdata
关闭 outputdata

其中,第 7 行调用计算 x 方在给定棋盘格局 board 下必赢首步的过程 FIND-WINNING-MOVE(board),是解决一个案例的关键

(2)处理一个案例的算法过程

对一个案例而言,将棋盘 board 中所有尚未下有棋子的格子(称为棋眼,设有 n 个)表示为一个数组 hole[1…n]。玩家 x 和 o 一个可能的下棋顺序恰是 hole[1…n]的一个全排列。固定 hole[1], hole[2…n]的所有排列对应的下棋方式若都使得 x 赢,则此 hole[1]就是要求的必赢策略的首步。而 hole[1]有 n 种不同的可能,所以,我们可以用如下的过程来计算 x 必赢首步。

FIND-WINNING-MOVE(board)
创建数组 hole←∅
for i←1 to 4
	do for j←1 to 4
		do if board[i, j]= "."
			then INSERT(hole, (i, j))
n←length[hole]
for i←1 to n
	do hole[1]↔hole[i]
		if 前一局 o 没赢 and 不是平局
			then x-force-win←TRUE
				return
			win-move←hole[1]
			x 在 hole[1]处下一棋子
			EXPLORE(hole, 2)
		hole[1]↔hole[i]

算法 4-14 解决“一步致胜”问题一个案例的算法

其中,第 10 行中访问的全局变量 x-force-win 表示一局中(按以固定的 hole[1]开头的hole[2…n-1]的所有排列对应的下棋方式), x 必赢的标志第 12 行中访问的全局变量 win-move表示一局棋的首步。第 14 行调用的 EXPLORE(k)过程为对当前的 hole[1…n-1]进行排列树回溯搜索算法,检测 x 以 hole[1]为首步的策略是否必赢。 FIND- WINNING-MOVE(board)过程运行结束时,若 x-force-win 为 TRUE,则 win-move 中存储的是 x 必赢首步信息。比照排列树问题算法框架的算法 4-10,回溯过程 EXPLORE 可描述如下:

EXPLORE(hole, k)
	n←length[hole]
	if k≥n
	then 作平局标志
		return
	for i←k to n-1
		do hole[i]↔hole[k]
		x 或 o 在 hole[k]处下棋子并检测记录是否赢
		if o 赢
			then return
		if x 赢
			then 清除 x 赢标志
			hole[i] ↔hole[k]
			continue this loop
	EXPLORE(hole, k+1)
	hole[i]↔hole[k]

算法 4-15 解决“一步致胜”问题中的回溯探索算法

#include <fstream>
#include <string>
#include <vector>
#include <bitset>
#include <iostream>

using namespace std;
bool checkrow(int i,bitset<16> &targ) //检测targ所示格局是否在第i行赢
{
	bitset<16> v(15 << (i * 4));
	return v == (targ & v);
}

bool checkcol(int j,bitset<16> &targ) //检测targ所示格局是否在第j列赢
{
	bitset<16> v(4369 << j);
	return v == (targ & v);
}

bool checkdiag(bitset<16> &targ) //检测targ所示格局是否在主对角线赢
{
	bitset<16> v(33825);
	return v == (targ & v);
}

bool checkdiag1(bitset<16> &targ) //检测targ所示格局是否在次对角线赢
{
	bitset<16> v(4680);
	return v == (targ & v);
}

class Player //玩家类
{
	bool win;//赢标志
	bitset<16> pattern;//自家格局
	bitset<16> initp;//自家初始格局
	void place(int i, int j);//在第i行第j列处下子
	void clear(int i, int j);//清除第i行第j列处棋子
	void reset();//用自家初始格局重置
	void reset(bitset<16> &p);//用格局参数重置
public:
	Player(int p = 0): win(false), initp(p), pattern(p) {}
	bool isWin() {return win;} //检测赢标志
	friend class TicTacToe;//探索一局棋
};

void Player::place(int i,int j)
{
	pattern.set(i * 4 + j); //设置格局对应位为1
	win = checkrow(i, pattern) || checkcol(j,pattern) || checkdiag(pattern) || checkdiag1(pattern);
}

void Player::clear(int i,int j)
{
	pattern.reset(i * 4 + j); //清除格局对应位上的1
}

void Player::reset()
{
	pattern = initp;
	win = false;
}

void Player::reset(bitset<16> &p)
{
	pattern = initp = p;
	win = false;
}

class TicTacToe //一字棋类
{
	Player x,o;//两个玩家
	vector<pair<int,int>> hole;//棋盘中可下子的棋眼
	vector<int> p;//下子顺序
	int n;//可下子数目
	bool draw;//平局标志
	void oneTurn(int k);//第k轮
	void restore(int k);//回溯前恢复格局
	void reset();//重置
	void explore(int k);
public:
	pair<int,int>winMove;//必赢首步
	TicTacToe(vector<string> &a);//构造函数
	friend bool findWinningMove(TicTacToe &game);//回溯搜索必赢首步
};

TicTacToe::TicTacToe(vector<string> &a)
{
	bitset<16> xinit = 0,oinit = 0;
	n = 0;
	for(int i = 0, t =0;i < 4;i++)
		for(int j = 0;j < 4;j++)
		{
			if(a[i][j] == '.') //记录可下子棋眼
			{
				hole.push_back(make_pair(i,j));
				p.push_back(n++);
				continue;
			}
			int k = i * 4 + j;
			if(a[i][j] == 'x') //跟踪x初始格局
				xinit.set(k);
			else //跟踪o初始格局
				oinit.set(k);
		}
		x.reset(xinit);//初始化x玩家
		x.reset(oinit);//初始化o玩家
		draw = false;//初始化各标志
		winMove = hole[p[0]];//初始化必赢首步
}

void TicTacToe::oneTurn(int k) //第k轮
{
	int i = hole[p[k]].first,j = hole[p[k]].second;
	if(k % 2 == 0) //轮到x玩家
	{
		x.place(i,j);
	}
	else //轮到o玩家
	{
		o.place(i,j);
	}
}

void TicTacToe::restore(int k) //恢复第k轮前格局
{
	int i = hole[p[k]].first,j = hole[p[k]].second;
	if(k % 2 == 0)
		x.clear(i,j);
	else
		o.clear(i,j);
}

void TicTacToe::reset()
{
	x.reset();
	o.reset();
	draw = false;
	winMove = hole[p[0]];
}

void TicTacToe::explore(int k)
{
	if(o.isWin())	//上一步o玩家赢,退出
	{
		return;
	}
	if(x.isWin()) 	//上一步x玩家赢,准备x的另一种走法
	{
		x.win = false;
		return;
	}
	if((k >= n) && !x.isWin() && !o.isWin()) //出现平局
	{
		draw = false;
		return;
	}
	for(int i = k;i < n;i++) //上一步未能决出胜负,准备进一步探索,共有n-k个可能的走法
	{
		swap(p[k], p[i]);//决定下子的棋眼
		oneTurn(k);//走一步
		explore(k + 1);//探索下一步
		restore(k);//回溯前恢复格局
		swap(p[k],p[i]);
	}
}

bool findWinningMove(TicTacToe &game)
{
	int n = game.n;
	for(int i = 0;i < n;i++) //n个可能的首步
	{
		swap(game.p[0],game.p[i]);
		if(i > 0 && !game.o.isWin() && !game.draw) //前一局x必赢
		{
			return true;
		} 
		else //或是第一局,或是前一局x非必赢
		{
			game.reset();//重置(x,o的格局恢复初始状态)
			game.oneTurn(0);//x玩家走第一步
			game.explore(1);//探索下一步
			game.restore(0);//恢复各玩家格局
			swap(game.p[0],game.p[i]);
		}
	}
	return false;
}

int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	char ch;
	inputdata >> ch;
	while(ch != '$')
	{
		string s;
		vector<string> a(4);
		for(int i = 0;i < 4;i++) //读取案例初始棋局
			inputdata>>a[i];
		TicTacToe game(a);//创建棋局
		if(findWinningMove(game)) //计算x必赢首步
		{
			outputdata << "(" << game.winMove.first <<","<<game.winMove.second<<")"<<endl;
			cout<<"(" <<game.winMove.first<<"."<<game.winMove.second<<")"<<endl;		
		}
		else
		{
			outputdata<<"#####" << endl;
			cout<<"#####" << endl;
		}
		inputdata>>ch;
	}
	inputdata.close();
	outputdata.close();
	return 0;
}

问题 4-5 订单

问题描述

商场经理对所有货物按表示其种类的标签的字母顺序分类。标签上首字母相同的货物存储于用同一字母标识的货仓。每天,商场经理要把接收到的货物订单一一登记。每一个订单仅需求一种货物。经理按登记的顺序处理订单安排发货。已经知道经理将处理完今天的所有订单,但并不知道这些订单的登记顺序。计算出经理一天内处理这些订单时所有可能的货仓访问顺序。

输入

输入仅含一行表示各订单所需所有货物的标签首字母(随机顺序)。所有字母都是小写的英文字母。订单数不超过 200。

输出

输出包含商场经理所有可能的对各货仓访问的顺序。每个货仓用一个小写英文字母表示,对货仓的访问顺序表示成这些英文字母组成的一个字符串。对各货仓的每个访问顺序写到输出文件中作为单独的一行。所有这些字符串按字典顺序排列输出(见输出样例)。输出文件的大小不超过 2MB。

输入样例

bbjd

输出样例

bbdj
bbjd
bdbj
bdjb
bjbd
bjdb
dbbj
dbjb
djbb
jbbd
jbdb
jdbb

解题思路

(1)数据输入与输出

由于输入文件仅有一行表示登记了的订单的字符串,从中读取这一个字符串 s,根据 s计算经理对货物所有可能的处理顺序。将计算结果按字典顺序按行写入输出文件。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取一行 s
result←ORDERS(s)
SORT(result)
for each r∈result
	do 将 r 作为一行写入 outputdata
关闭 inputdata
关闭 outputdata

其中,第 4 行调用计算货物处理顺序列表的过程 ORDERS(s)是解决一个案例的关键。

注意到所有订单符号序列 s 中有重复的字符,所以这是一个可重元素集合的全排列问题。解决这一问题最简单的办法是计算出订单符号的全排列,从中剔除重复的序列(例如设置一个无重复元素的集合;用来存储排列结果)。也可以采取以下的方法:对于订单数据 s,我们将其中不同的字符析取出来,组成集合 label,并记录下每个符号的重复个数。例如,输入案例中 label ={(“b”, 1), (“d”, 0), (“j”, 0)}}。对 label 中的字符构成序列调用算法 4-10 计算出所有的全排列,存于集合 b。然后,对 b 中每个序列,将 label 中有重复的一个字符插入到所有可能的位置上加以拓广(该字符的重复数自减 1),形成新长度增加 1 的序列集合 b。循环往复,直至 label 中所有字符的重复数为 0 。返回 b 即为所求。

ORDERS(s)
m←length[s]
label←s 中所有不同字符 c 及其重复个数 n
x←label 中的所有字符构成的序列
PERMUTATION-TREE(x, 1)
b←solution
k←length[label]
while k<m
	do for each s∈label
		do if n[s]>0
			then c←c[s], n[s]←n[s]-1
				 k←k+1, b1←∅
			for each x∈b
				do n←length[x]
				   j←1
				while j≤n+1
					do x1←x
						if j≤n
							then 在 x1 中 x1[j]前插入 c
							else 在 x1 尾部添加 c
						INSERT(b1, x1)
					b←b1
return b

算法中第 4 行调用算法 4-10 (将第 1 行的检测条件简化为 k >label 中元素个数,删除第 6 行的检测条件))对 label 中的字符构成序列计算出所有的全排列存于 solution,第 7~ 21 行的 while 循环对 b 中的序列加以扩展:将 label 中有重复的字符依次插入到原序列的各个位置形成完整的订单序列。虽然这是一个 4 重循环,但产生的全排列不超过 m ! ,故算法 4-16 的运行时间为 Θ (m!) 。

#include <fstream>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	string s;
	inputdata>>s;
	sort(s.begin(),s.end());
	outputdata<<s<<endl;
	cout<<s<<endl;
	while(next_permutation(s.begin(),s.end()))
	{
		outputdata<<s<<endl;
		cout<<s<<endl;
	}
	inputdata.close();
	outputdata.close();
	return 0;
}

子集树问题

而对于像 0-1 背包问题那样的如何在一个集合当中选取一个子集合这样的组合问题,解向量x=<x1 , x2, … , x n> 中的每一个分量 x k∈{0, 1} ( 1≤k≤n) 。此时,回溯算法可简化为如下形式。

SUBSET-TREE(x, k)
	if IS-COMPLET(x, k)
		then INSERT(solutions, x)
			return
 for i←0 to 1 对当前第 k 个分量逐一检测两种可能的情形
	do xk←i
		if IS-PARTIAL(x, k)
			then SUBSET-TREE(x, k+1)

子集树算法框架

问题 4-6 命题逻辑

问题描述

命题是由命题符号及连接词构成的逻辑表达式。命题可以如下递归地进行定义:

所有命题符号(本题中指的是小写的英文字母,即 a~z)是命题。若 P 是一个命题,则(!P)是一个命题,且称 P 是该命题的直接子命题。若 P 及 Q 都是命题,则(P&Q),(P|Q) ,(P–>Q)及(P<->Q)都是命题,且称 P 和Q 是它们的直接子命题。其他的都不是命题。连接词“!”“&”“!”“ > ”及“<−>”分别表示逻辑非、合取、析取、蕴含和等价。命题 P 是命题 R 的子命题,指的是 P=R 或 P 是 Q 的直接子命题、而 Q 是 R 的子命题。设 P 为一个命题并对 P 中所有命题符号指派布尔3值(0 或 1)。这将导致命题中的所有子命题按下列表格的计算意义都获得布尔值。

合取析取蕴含等价
!0=10&0=000=00–>0=1
!1=00&1=001=10–>1=1
1&0=010=11–>0=01<->0=0
1&1=111=11–>1=11<->1=1

按此方法,我们可以计算出 P 的值。这个值依赖于对各命题符号的布尔值指派。若 P含有 n 个不同的命题符号,则有 2n 个不同的布尔值指派方案。可以用真值表来表示所有的布尔值指派。一个真值表包含 2n 行,每行表示一个布尔值指派下各个子命题的布尔值。命题符号的布尔值写在该符号的下方,连接词的布尔值写在该连接词下方正中。

输入

输入包含若干个测试案例。每个案例占一行,包含一个命题,其中每个命题符号、连接词以及括号之间用空格隔开。

输出

对每个测试案例,创建一个真值表。真值表的顶部为命题本身。对每一个布尔值指派计算该命题(包括其子命题)的值,并作为一行加以输出。输出行应与输入的表达式对齐(必要的地方加上空格)。案例之间输出一个空行。设 s1,…,sn 为命题中所有符号按字母表顺序排列的序列。对它们的布尔值指派需按 si取 0 先于取 1,i=1,2,…,n。

输入样例

((b --> a) <->((!a)–>(!b)))

((y & a)–>(c|c))

输出样例
((b --> a)<->((!a)–>(! b)))
0 1 0 1 1 0 1 1 0
1 0 0 1 1 0 0 0 1
0 1 1 1 0 1 1 1 0
1 1 1 1 0 1 1 0 1
((y & a) -->(c |c))
0 0 0 1 0 0 0
1 0 0 1 0 0 0
0 0 0 1 1 1 1
1 0 0 1 1 1 1
0 0 1 1 0 0 0
1 1 1 0 0 0 0
0 0 1 1 1 1 1
1 1 1 1 1 1 1

解题思路

(1)数据输入与输出

按输入文件格式,每行一个测试案例。依次从输入文件中读取每个案例表示命题的字符串 s,计算出该命题对其中所含命题符号的所有指派下各子命题的真值,按指定的格式(命题串开头,以后每行表示一个指派下的真值)写入输出文件。循环往复,直至输入文件结束。

打开输入文件 inputdata
创建输出文件 outputdata
while 能从 inputdata 中读取一行 s
	do result←BOOLEAN-LOGIC(s)
		将 s 作为一行写入 outputdata
		for each r∈result
			do 将 r 作为一行写入 outputdata
		向 outputdata 写入一空行
关闭 inputdata
关闭 outputdata

其中,第 4 行调用计算命题 s 真值表的过程 BOOLEAN-LOGIC(s)是解决一个案例的关键。

(2)处理一个案例的算法过程

​ 对于一个案例数据 s,实质上是命题的中缀表达式。本题的任务是计算该表达式中对命题符号的各种可能指派,计算其中各自命题的布尔值。我们在上一章曾经利用二叉树来表示一个表达式,并可借以计算表达式的值。此处,我们依法炮制。现将 s 转换成对应的二叉树。二叉树中的每个节点表示一个子命题:连接词为树根 root,左值为左子树 left,右值为右子树 right。

特殊地,命题符号为树根,左右孩子均为空。我们约定,对于单元连接词“ !”构成的子命题,左子树为空,仅有右子树。由于 s 中所有子命题均带括号,故无需区分各连接词的优先级.

TO-PROPOSITION(s)
operands←∅
operators←∅
while 能从 s 中析取一项 item
	do if item 为一个命题符号
		then 创建 left、right 均为 NIL,root 为 item 的二叉树 p
			PUSH(operators, p)
		else if item=" ("
			then 将 item 压入 operators
			else t←POP(operators)
				while t≠" ("
					do r←POP(operands)
						if t≠ "!"
							then l←POP(operands)
							else l←NIL
						创建以 item 为 root,l、r 为 left、right 的二叉树 p
						PUSHU(operands, p)
						t←POP(operators)
					PUSH(operators, item)
return POP(operands)

根据命题中缀式 s 构造对应二叉树的算法过程

利用算法 4-18 构造的表示命题的二叉树 proposition,对命题中所有命题符号的任一指派appoint,就可通过对 proposition 的有序遍历来计算每个子命题的布尔值。假设二叉树中每个节点增设了一个 value 属性。

CALCULATE(proposition, appoint)
if left[proposition]=NIL and right[proposition]=NIL
	then value[proposition]←root[proposition]在 appoint 中指定的值
	else if root[proposition]= "!"
		then CALCULATE (right[proposition], appoint)
			value[proposition]!value[right[poropsition]]
		else CALCULATE(left[proposition], appoint)
			CALCULATE(right[proposition], appoint)
			value[proposition]←value[left[poropsition]]与 value[right
			[poropsition]]相应运算

对命题 proposition 中的符号给定指派 appoint 计算各子命题布尔值的算法过程

假定 proposition 中共有 n 个各不相同的命题符号,算法 4-19 中的表示 proposition 中各命题符号的所有布尔值指派 appoint 可以通过修改算法 4-17 得到。

SUBSET-TREE(x, k)
	if k>n
		then INSERT(appoints, x)
		return
for i←0 to 1 对当前第 k 个分量逐一检测两种可能的情形
	do xk←i
		then SUBSET-TREE(x, k+1)

计算 n 个命题符号所有布尔值指派的回溯算法

算法 4-20 从 k=1 开始,直至运算结束,appoints 中保存了 2n 个长度为 n 的 0-1 序列,也就是 proposition 中 n 个命题符号的 2n 个指派。对指定的布尔值指派 appoint 运行算法 4-19 后,存储于各节点中的 value 属性按中序顺序构成的序列就是命题真值表中的一行。回忆上一章中关于二叉树的中序遍历,我们有如下过程。

GET-VALUE(proposition)
if left[proposition]≠NIL
	then GET-VALUE(left[proposition])
INSET(values, value[proposition])
if right[proposition]≠NIL
	then GET-VALUE(right[proposition])

获取子命题布尔值序列的算法过程

算法 4-21 运行完毕,values 中保存了各子命题(包括命题符号)的布尔值构成的中序序列。利用算法 4-18~算法 4-21,我们可以描述如下的解决本问题一个案例的算法过程。

BOOLEAN-LOGIC(s)
proposition←TO-PROPOSITION(s)
n←s 中命题符号的个数
appoints←∅
4 SUBSET-TREE(x, 1)
table←∅
for each appoint in appoints
	do CALCULATE(proposition, appoint)
		values←GET-VALUE(proposition)
		INSERT(table, values)
return table

算法 4-22 解决“命题逻辑”问题一个案例的算法过程

由于第 4 行调用了耗时为 Θ ( 2 n ) Θ (2^n) Θ(2n)的子集树回溯算法过程,所以算法 4-22 的运行时间为为 Θ ( 2 n ) Θ(2^n) Θ(2n)

问题 4-7 整除性

描述
考虑任意整数序列,在数项之间可以加入“+”或“−”,形成一个算术表达式。不同的表达式算得不同的值。例如,对序列 17, 5, −21, 15,有如下所示的 8 个可能的表达式:
17 + 5 + −21 + 15 = 16
17 + 5 + −21 − 15 = −14
17 + 5 − −21 + 15 = 58
17 + 5 − −21 − 15 = 28
17 − 5 + −21 + 15 = 6
17 − 5 + −21 − 15 = −24
17 − 5 − −21 + 15 = 48
17 − 5 − −21 − 15 = 18

如果在一个整数序列中加入“+”或“−”使得计算结果值能被 K 整除,则称该序列能被 K 整除。在上述的例子中,序列能被 7 整除(17+5+−21−15=−14)但不能被 5 整除。
你要写一个程序确定整数序列的整除性。
输入
输入文件中包含若干个测试案例。每个测试案例的第一行包含两个整数 N 及 K( 1≤N≤10000, 2 ≤K≤ 100),两数用空格隔开。N=0 为输入结束标志。第二行包含 N 个整数构成的序列。整数之间用空格隔开,各整数的绝对值不超过 10000。
输出
若给定的序列能被 K 整除,向输出文件写入一行“ Divisible ”,否则输出一行“Not divisible”。
输入样例
4 7
17 5 -21 15
0
输出样例
Divisible
解题思路
(1)数据输入与输出
按输入文件格式,依次从中读取整数 N 和 K。然后读取 N 个整数保存在数组 a 中。对数组 a 计算连接 a 中 N 个数据不同运算符序列的运算结果,并检测是否能被 K 整除。计算结果保留在 result 中,根据 result 决定写入输出文件的内容:若 result 为 true,输出一行“Divisible”;否则输出一行“Not divisible”。循环往复,直至读到 N=0。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 N
while N>0
	do 创建数组 a[1..N]
		从 inputdata 中读取 K
		for i←1 to N
			do 从 inputdata 中读取 a[i]
			result←DIVISABLE(a, K)
			if result=true
				then 将"Divisible"作为一行写入 outputdata
				else"Not divisible"作为一行写入 outputdata
		从 inputdata 中读取 N
关闭 inputdata
关闭 outputdata

其中,第 9 行调用检测 a 中数据用 N−1 个加、减号连接的运算结果能否被 K 整除的过程 DIVISABLE(a, K)是解决一个案例的关键。

(2)处理一个案例的算法过程
数组 a 和模数 K,由于 N−1 个运算符只用“+”或“−”,所以将各运算符对应一个 0-1序列:0 对应“+”,1 对应“−”。这样就可以运用算法 4-17 生成这 2 N 个序列

DIVISABLE(a, K)
 N←length[a]
solutions←∅
SUBSET-TREE(x, k)
for each x∈solutions
	do sum←a[1]
		for i←1 to N-1
			do if x[i]=0
				then sum←sun+a[i+1]
				else sum←sun-a[i+1]
if sum Mod K≡0
	then return true
 return false

算法 4-23 解决“整除性”问题一个案例的算法过程

算法中第 3 行调用的 SUBSET-TREE( x, k) 过程是算法 4-17 经过修改的算法 4-20, 耗时 Θ ( 2 N ) Θ (2^N) Θ(2N) ,所以算法 4-23 的运行时间是 Θ ( 2 N ) Θ (2^N) Θ(2N)。本题中数组 a 的元素个数 N(1≤N≤ 10000)可能使得算法的运行时间十分惊人。为减少实际要考察的元素个数,进而改善算法的运行时间,可以对 a 做如下的预处理:将每一个整数 a[ i] ( 1 ≤i≤N)替换成 a[i] Mod K,即 a[ i] 除以 K 的余数。若 a [i]除以 K 的余数为 0,则意味着 a[ i] 能被 K 整除,将其从 a 中剔除。若有 a[i]=a[j ](i≠ j) ,则意味着两者之差能被 K 整除,故可将两者从 a 中剔除。此外,若有 a[ i]+a [j ]=K(i≠j) ,意味着两者之和能被 K 整除,也可将两者从 a 中剔除。再对缩小了规模的数组 a 运行算法 4-23 可改善运行效率。例如,对输入样例的 a={17, 5, −21, 15} 及 K=7,每个元素替换为自身除以 7 后得到 a ={3, 5 ,0, 1},剔除元素 0,得 a ={3, 5, 1} 。由于 3+5−1=7=K,故答案是“Divisible”

#include <fstream>
#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <bitset>
#include <algorithm>

using namespace std;
vector<int> preprocess(vector<int>&a,int K)
{
	set<int> S;
	for(int i=0;i<a.size();i++)
	{
		a[i]=a[i]%K;
		if(a[i]!=0)
		{
			if(S.find(a[i])!=S.end())
			{
				S.erase(a[i]);
				continue;
			}
			bool insert=true;
			for(set<int>::iterator it=S.begin();it!=S.end();it++)
			{
				if((*it-a[i])%K==0 || (*it+a[i])%K==0)
				{
					S.erase(it);
					insert=false;
					break;
				}
			}
			if(insert)
				S.insert(a[i]);
		}
	}
	vector<int>b;
	for(set<int>::iterator x = S.begin();x!=S.end();x++)
	{
		b.push_back(*x);
	}
	return b;
}

bool divisible(vector<int>&a,int K)
{
	vector<int> b = preprocess(a,K);
	int n = b.size();
	if(n == 0)
		return true;
	if(n < 2)
	{
		return false;
	}
	int p = 1;
	for(int i = 0;i < n - 1;i++)
	{
		p *= 2;
	}
	for(int i = 0;i < p;i++)
	{
		bitset<32> op(i);
		int sum = b[0];
		for(int j = 1;j < n;j++)
		{
			if(op[j - 1] == 0)
			{
				sum+=b[j];
			}
			else
			{
				sum -= b[j];
			}
		}
		if(sum % K == 0)
		{
			return true;
		}
	}
	return false;
}
int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	int N;
	inputdata>>N;
	while(N > 0)
	{
		int K;
		inputdata >> K;
		vector<int> a[N];
		for(int i = 0;i < N;i++)
		{
			inputdata>>a[i];
			bool result =divisible(a,K);
			if(result)
			{
				outputdata<<"Divisable "<<endl;
				cout<<"Divisable"<<endl;
			}
			else
			{
				outputdata<<"Not divisible"<<endl;
				cout<<"Not divisible"<<endl;
			}
			intputdata>>N;
		}
		inputdata.close();
		outputdata.close();
		return 0;
	}
}

用回溯算法解组合优化问题

可以利用回溯算法解决组合优化问题。不难得知,组合优化问题实际上是在合法解中寻找最优解。所以,设置一个最优解 xopt 及其目标值 value(若是最小化问题初始化为∞,若是最大化问题初始化为−∞)。同时,为每一个正在探索中的解 x 设置一个目标值 current-value。探索过程中动态计算 current-value,一旦得到一个完整合法解,就与 value 比较,决定取舍算法结束时,跟踪到的 xopt 及 value 就是最优的解及其目标值。

例如,对 0-1 背包问题,若每件物品 tk 除了具有重量 wk 以外还具有价值 vk, (1≤k≤n),要求计算窃贼如何行动才能使带走的东西价值最大。这是一个典型的组合优化问题。我们只要对算法 4-3 稍做修改就能得出最优解及其目标值。设置全局量 xmax、current-value、weight和 value。将 current-value 和 weigh t 初始化为 0,将 value 初始化为−∞。

KNAPSACK(x, k)
if k>n 判断是否为完整解
	then if current-value>max-value
		then xmax←x, max-value←current-value
	return
for i←0 to 1 对当前第 k 个物品逐一检测两种可能的情形
	do xk ←i
		if weight+x[k]*w[k] ≤C 部分合法
			then weight←weight+x[k]*w[k]
				current-value←current-value+x[k]*v[k]
				KNAPSACK(x, k+1) 进入下一层搜索
				current-value←current-value-x[k]*v[k]
				weight←weight-x[k]*w[k]

0-1 背包问题(组合优化)回溯算法

​ 在布加勒斯特的商业中心有一个很大的银行,银行有一个巨大的地下金库。金库里有 N 个编号为 1~N 的保险柜。第 k 号保险柜中保存着 k 块钻石,每块重 wk、价值 ck。约翰和布鲁斯设法潜入了金库。他们当然想拿走所有的钻石,无奈这两个家伙力气有限,最多只能带走重量为 M 的物品。你的任务是帮助约翰和布鲁斯在那些保险箱中选取钻石,使得总重量不超过 M,而价值最大。输入输入的第一行仅含一个整数 T——测试案例个数。每个测试案例的第一行含两个整数 N和 M,两数之间用空格隔开。接着的一行包含 N 个用空格隔开的整数表示 wk。测试案例的最后一行也包含 N 个用空格隔开的整数表示 ck。输出对每一个测试案例输出一行包含一个表示能带走的钻石最大价值的整数。输入样例

2
2 4
3 2
5 3
3 100
4 7 1
5 9 2

输出样例

6
29
解题思路
(1)数据的输入与输出
根据输入文件格式,先从中读取案例数 T。然后依次读取各案例的数据,首先读取保险柜数和盗贼能背走得最大重量 N 和 M。接着读取 N 个保险柜中钻石块的重量,保存在数组w[1…N]中。再读取 N 个保险柜中钻石块的价值,保存于数组 c[1…N]中。对案例数据 w,c 和M,计算盗贼能带走的最大价值。将计算结果作为一行写入输出文件中。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 T
	for t←1 to T
		do 从 inputdata 中读取 N, M
            创建数组 w[1..N]
            for i←1 to N
    创建数组 c[1..N]
    for i←1 to N
    	do 从 inputdata 中读取 c[i]
    result←THE-ROBBERY(w, c, M)
    将 result 作为一行写入 outputdata
关闭 inputdata
关闭 outputdata

其中,第 12 行调用的计算盗贼能带走的钻石的最大价值的过程 THE-ROBBERY(w, c, M)是解决一个案例的关键

(2)处理一个案例的算法过程
对于一个测试案例,由于第 k 个盒子里有 k 块钻石( 1≤k≤N),每块钻石的重量为 wk,因此这个盒子里的钻石重量可表示为含有 k 个元素的集合 w = { w k , w k , . . . , w k } ⏟ k w=\underbrace{\left\{w_k,w_k,...,w_k\right\}}_\text{k} w=k {wk,wk,...,wk} 。相仿地,第 k 个盒子中的k 块钻石的价值可表为集合 c = { c k , c k , . . . , c k } ⏟ k c=\underbrace{\left\{c_k,c_k,...,c_k\right\}}_\text{k} c=k {ck,ck,...,ck} 。于是,可将问题转换为如下的 0-1 背包问题,即
W = { w 1 , w 2 , w 2 , . . . , w k , . . . , w k ⏟ K , w n , . . . , w n ⏟ N } W=\left\{w_1,w_2,w_2,...,\underbrace{w_k,...,w_k}_K,\underbrace{w_n,...,w_n}_N\right\} W=w1,w2,w2,...,K wk,...,wk,N wn,...,wn

c = { c 1 , c 2 , c 2 , . . . , c k , . . . , c k ⏟ K , c n , . . . , c n ⏟ N } c=\left\{c_1,c_2,c_2,...,\underbrace{c_k,...,c_k}_K,\underbrace{c_n,...,c_n}_N\right\} c=c1,c2,c2,...,K ck,...,ck,N cn,...,cn

其中,W 为物品的重量集合;C 为物品的价值集合;M 为背包承重量。例如,输入样例中的第一个案例,就对应如下的 0-1 背包问题:
W = { 3 , 2 , 2 } , C = { 5 , 3 , 3 } , M = 4 W=\left\{3,2,2\right\},C=\left\{5,3,3\right\},M=4 W={3,2,2},C={5,3,3},M=4
调用算法 4-24,解此背包问题,所得即为所求。

THE-ROBBERY(w, c, M)
	N←length[w], n←N*(N+1)/2
	W←∅, C←∅
	for k←1 to N
		do for j←1 to k
			do APPEND(W, w[k])
			   APPEND(C, c[k])
创建 x[1..n]
current-value←0, weight←0, value←-KNAPSACK(x, 0)
return value

算法 4-25 解决“盗贼”问题一个案例的算法过程

算法中第 9 行调用算法 4-24 的 KNAPSACK(x, 0),耗时 Θ ( 2 n ) Θ(2^n) Θ(2n),故运行时间为 Θ ( 2 n ) Θ(2^n) Θ(2n)。解

#include <fstream>
#include <iostream>
#include <vector>
using namespace std;
class Bank
{
private:
	vector<int>weight;
	vector<int>cost;
	vector<int>x;
	int value;
	int w;
	int m;
	int n;
	int maxValue;
	void knapsack(int k);
public:
	Bank(vector<int> &W,vector<int>&C,int M);
	friend int theRoberry(vector<int>&W,vector<int> &C,int m);
};

Bank::Bank(vector<int> &W,vector<int> &C,int M)
{
	int N = W.size();
	n = N * (N + 1)/2;
	weight = vector<int>();
	cost = vector<int>();
	x = vector<int>();
	m = M;
	value = 0;
	w = 0;
	maxValue = INT_MIN;
	for(int i = 0;i < N;i++)
	{
		for(int j = 0;j <= i;j++)
		{
			weight.push_back(W[i]);
			cost.push_back(C[i]);
		}
	}
}
void Bank::knapsack(int k)
{
	if(k >= n)
	{
		if(maxValue < value)
			maxValue = value;
		return ;
	}
	for(int i = 0;i < 2;i++)
	{
		x[k] = i;
		if((w + x[k]*weight[k]) <= m)
		{
			w += x[k] * weight[k];
			value += x[k] * cost[k];
			knapsack(k + 1);
			w -= x[k] * weight[k];
			value -= x[k] * cast[k];
		}
	}
}
int theRoberry(vector<int> &W,vector<int>&C,int m)
{
	Bank bank(W,C,m);
	bank.knapsack(0);
	return bank.maxValue;
}

int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	int T;
	inputdata>>T;//读取案例数
	for(int t = 0;t < T;t++)//处理每个案例
	{
		int n,M;
		inputdata >> N >> M;//读取柜子数和最多能带走的重量
		vector<int> W(N),C(N);
		for(int i = 0;i < N;i++)
			inputdata>>W[i];
		for(int i = 0;i < N;i++)
			inputdata>>C[i];
		int m = theRoberry(W,C,M);
		outputdata<<m<<endl;
		cout<<m<<endl;
	}
	inputdata.close();
	outputdata.close();
	return 0;
}

问题 4-9 牛妞玩牌

问题描述

晚夏时节的农场,时光显得如此缓慢。牛妞 Betsy 无所事事,独自玩一种叫作 solitaire 的扑克牌游戏以消磨时间。众所周知,牛妞的智商与人类的智商不能同日而语。所以与人类玩的 solitaire 牌不同, Betsy 玩的 solitaire 牌没什么挑战性。牛妞玩的 solitaire,用 N×N (3 ≤ N ≤ 7)张普通的扑克牌(4 种花色:梅花、方块、红心及黑桃, 13 种点数: A, 2, 3, 4, …, 10, J, Q, K)摆成一个方阵。每块牌用点数(A, 2, 3, 4, …10, J, Q, K)跟花色(C, D, H, S)表示。下列方阵即为一个 N = 4 的例子:

8S AD 3C AC(黑桃八,方块 A,…)
8C 4H QD QS
5D 9H KC 7H
TC QC AS 2D

玩此 solitaire 时, Betsy 从方阵的左下角开始(TC)并做 2*N-2 次或“向右”或“向上”的移动,来到右上角。在此行进的过程中,Betsy 将经过的每张牌的点数累加起来(A 表示点数 1,2,3,…,9 表其自然点数,T 表示 10 点,J 表示 11 点,Q 表示 12 点,K 表示 13点)。她的目标是得到最大的总点数。若 Betsy 经过的路径为 TC-QC-AS-2D-7H-QS-AC ,则她得到的总点数是 10+12+1+2+7+12+1=45。若路径为 TC-5D-8C-8S-AD-3C-AC,则得到的总点数为 10+5+8+8+ 1+3+1=36,没有刚才那条路径好。此方阵中最好的成绩应该是 69 点(TC-QC-9H-KC-QD-QS -AC ⇒10+12+9+13+12+12+1)。Betsy 就是想知道她能得到的最好成绩是多少。有个傻牛妞曾经告诉过 Betsy 一个秘技:“从结果回到开始”。但是 Betsy 百思不得其解。

输入
*第 1 行:仅含一个整数 N。
*第 2~N+1 行:第 i+1 行罗列出方阵中的第 i 行的 N 块牌。牌面用上述的点数跟花色的
方式表示。
输出
*仅含一行:一个表示 Betsy 能得到的最好成绩的整数。

输入样例
4
8S AD 3C AC
8C 4H QD QS
5D 9H KC 7H
TC QC AS 2D
输出样例
69
解题思路
(1)数据的输入与输出
根据输入文件格式,首先从中读取方阵规模 N。接下来依次从输入文件中 N 行数据中每行读取 N 组,丢弃其中表示牌的花色的符号,保留表示牌的点数的整数,组织成一个方阵(二维数组)A[1…N, 1…N]。计算从 A 的左下角(A[N, 1])开始向右或向上走 2N-1 步,走到方阵右上角(A[1, N])经过的路径的最大累加值。将计算所得结果作为一行写入输出文件。

打开输入文件 inputdata
创建输出文件 outputdata
从 inputdata 中读取 N
创建数组 A[1..N, 1..N]
for i←1 to N
    do for j←1 to N
    do 从 inputdata 中读取一项 x
        从 x 中解析出牌点 v
        A[i, j]←v
result←COW-SOLITAIRE(1)
将 result 作为一行写入 outputdata
关闭 inputdata
关闭 outputdata

其中,第 10 行调用计算从方阵左下角走到右上角所经路径最大累加值的过程 COW-SOLITAIRE(1)是解决一个案例的关键。

(2)处理一个案例的算法过程

设牌局的点值方阵记为 A,其行、列编号与普通矩阵相同:行自上而下为 1,…,N,列自左向右也为 1,…,N。方阵中第 i 行、第 j 列位置表为<i, j>,点值为 A[i, j]。牛妞玩牌的一条合法路径可表为向量 x=<x1, x2, …, x2N−1>。其中 xk=<i, j> (1≤k≤ 2N−1, 1≤ i, j ≤N)。< x1, x2, …, xk>合法,当且仅当 xk 位于 xk−1 之上或右边。因此,从 xk−1 走到 xk 只需考虑合法的情形:

1 若 i=1,即当前位置在方阵的顶部。这时只能向右走一步,即 j←j +1。
2 若 j=N,即当前位置在方阵的右边缘。这时只能向上走一步,即 i←i−1。
3 1<i <N,1<j<N。即当前位置在方阵内部。这时有两种走法:向右一步或向上一步。
我们约定先向右走一步探索,然后回到原地再向上走一步探索
部分解 < x 1 , x 2 , . . . , x k > <x_1, x_2, ..., x_k> <x1,x2,...,xk>的目标值为 ∑ t = 1 k A [ i , j ] \sum^k_{t=1}A[i,j] t=1kA[i,j],其中 < i , j > = x ( t 1 ≤ t ≤ k ) <i, j>=x(t 1≤t≤k) <i,j>=x(t1tk) 。设置全局量 value,表示最优解的目标值,初始化为−∞;设置变量 current-value,表示当前解的目标值,初始化为 A[N, 1];设置变量 i, j,表示当前位置,初始化为 N, 1 。由于本题仅关心最优解的目标值,甚至无需记录解向量,仅动态地记录当前解的目标值。算法伪代码如下。

COW-SOLITAIRE(k)
if k>2N-1
    then if current-value>value
        then value←current-value
    return
if i=1
then j←j+1 已到顶部,只能向右走一步
    current-value← current-value+A[i, j]
    COW-SOLITAIRE(k+1)
    current-value← current-value-A[i, j]
    j←j-1
else if j=N 已到右边缘,只能向上走一步
    then i←i-1
        current-value← current-value+A[i, j]
        COW-SOLITAIRE(k+1)
        current-value← current-value-A[i, j]
        i←i+1
    else j←j+1 既要向右走
        current-value← current-value+A[i, j]
        COW-SOLITAIRE(k+1)
        current-value← current-value-A[i, j]
        j←j-1, i←i-1 也要从原地向上走
        current-value← current-value+A[i, j]
        COW-SOLITAIRE(k+1)
        current-value← current-value-A[i, j]
        i←i+1

由于每一步有 2 种不同的走法,一共要走 2N-1 步,所以检测的计算要做 22N−1 次。因此,算法 4-26 的运行时间为Θ(2N)。

#include <iostream>
#include <fstream>
#include <vector>
#include <climits>
using namespace std;
int getNumber(char c)
{
	switch(c)
	{
		case 'A':return 1;
		case 'T':return 10;
		case 'J':return 11;
		case 'Q':return 12;
		case 'K':return 13;
		default:return c - '0';
	}
}
class Solitaire
{
	vector<vector<int>> A;
	int n,i,j;
public:
	int value,currentValue;
	Solitaire(vector<vector<int>>a):A(a),n(a.size()),value(INT_MIN),currentValue(a[n-1][0]),i(n-1),j(0){}
	void cowSolitaire(int k);
};

void Solitaire::cowSolitaire(int k)
{
	if(k>=2*n-1) //走到右上角
	{
		if(currentValue>value)
			value = currentValue;
		return;
	}
	if(i==0) //顶层只能向右走
	{
		j++;
		currentValue+=A[i][j];
		cowSolitaire(k+1);
		currentValue-=A[i][j];
		j--;
	}
	else if(j==n-1)//右边缘只能向上走
	{
		i--;
		currentValue+=A[i][j];
		cowSolitaire(k+1);
		currentValue-=A[i][j];
		i++;
	}
	else
	{
		j++;//向右走
		currentValue+=A[i][j];
		cowSolitaire(k+1);
		currentValue-=A[i][j];
		i--;//向上走
		j--;
		currentValue+=A[i][j];
		cowSolitaire(k+1);
		currentValue-=A[i][j];
		i++;
	}
}
int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	int N;
	char *s = new char[3];
	inputdata>>N;
	vector<vector<int>> a(N,vector<int>(N));
	for(int i = 0;i<N;i++)
	{
		for(int j = 0;j<N;j++)
		{
			inputdata>>s;
			a[i][j]=getNumber(s[0]);
		}
	}
	delete[]s;
	Solitaire sol(a);
	sol.cowSolitaire(1);
	outputdata<<sol.value<<endl;
	inputdata.close();
	outputdata.close();
	return 0;
}

问题 4-10 三角形游戏

问题描述有

6 个正三角形,三角形的每条边都有编号,如图 4-7所示。可以平移、旋转每一个三角形,使它们形成一个正六边形。构成的六边形为合法的,要求任意两个相邻三角形的公共边具有相同的编号。游戏中不能将三角形翻转。图 4-8展示了两个合法的正六边形。六边形的得分是外边沿的六条边的编号相加之和。我们的任务是找出由 6 个三角形形成的合法六边形的最高得分。输入输入包含若干个测试案例。每个案例包含 6 行数据,每行有 3 个介于 1~100 的整数,按顺时针方向表示一个三角形 3 条边的编号, 3 个整数之间用空格隔开。测试案例之间由仅含一个星号的一行隔开。最后一个案例之后一行仅含一个美元符。

在这里插入图片描述

在这里插入图片描述

输出

对输入的每一个测试案例,若不存在合法的六边形,则输出一行“none”的信息,否则输出一行含合法六边形最高得分的数据。

输入样例
1 4 20
3 1 5
50 2 3
5 2 7
7 5 20
4 7 50
*
10 1 20
20 2 30
30 3 40
40 4 50
50 5 60
60 6 10
*
10 1 20
20 2 30
30 3 40
40 4 50
50 5 60
10 6 60
$

输出样例
152
21
none
解题思路
(1)数据的输入与输出
按输入文件的格式,依次读取每个测试案例的数据。每个案例有 6 行数据,每行描述一个三角形的三条边的长度。将这 6 组数据保存在数组 triple[1…6]中。对案例数据 triple,计算符合体面要求的六角形的最大周长。若存在符合要求的六边形,计算结果为最大的周长,否则为“none” 。将计算结果作为一行写入输出文件。一行仅含“*”作为两个案例的分隔, “$”为输入文件的结束标志。

打开输入文件 inputdata
创建输出文件 outputdata
ch←"*"
while ch≠"$"
	do 创建数组 triple[1..6]
		for i←1 to 6
			do 从 inputdata 中读取 a, b, c
				t[i](a, b, c)
		result←THE-TRIANGLE-GAME(2)
		将 result 作为一行写入 outputdata
		从 inputdata 中读取 ch
关闭 inputdata
关闭 outputdata

其中,第 9 行调用计算合法六边形最大周长的过程 THE-TRIANGLE-GAME(2)是解决一个案例的关键。由于要对 6 个三角形考察所有可能的摆放形式(环状排列:固定第一个元素,其余 5 个元素的全排列),所以是一个排列树回溯算法,顶层调用从 k=2 起

(2 )处理一个案例的算法过程对每一个测试案例, 6 个三角形可视为 6 个元素{t1, t2, t3,t 4,t 5, t6}的环状排列(见图 4-9),共有 5!个不同情形(固定第一个元素,其余 5 个元素的全排列即构成 6 个元素的环状排列)。对于一个排列,相邻两个三角形的摆放方向不同,一个底在下,一个底在上。每个三角形按边的顺序有 3 种不同的摆放方式(见图 4-10),共有 3 6 3^6 36 种不同的情形。我们的目标是在这5! 3 6 3^6 36 个不同情形中找出所有合法的摆放方式(相邻边的值相同),并计算出外围边长之和的最大者.

在这里插入图片描述

这样我们必须逐一找出{t1, t2, t3,t4, t5, t6}的 5!个环状排列,这可以用一个回溯算法算得

THE-TRIANGLE-GAME(k)
if k>6
	then PLAY(1)
	return
for i ← k to 6
	do ti ↔tk
	THE-TRIANGLE-GAME (k+1)
	ti ↔tk

其中,第 3~4 行中访问的变量 max 是一个全局量,对每一个测试案例,max 初始化为 − ∞。第 7 行调用过程 ROTATE(tk)将三角形 tk 顺时针旋转 120°,变换一个摆放方式。对一个具有合法六边形的案例,输出最终算得的 max,否则(max 保持为−∞)输出“none” 。由于算法 4-28 的运行时间为 3 6 3^6 36,而算法 4-27 除了自身递归 5!次以外还在第 2 行调用了算法 4-28,故其运行时间为 5! 3 6 3^6 36

#include <fstream>
#include <iostream>
#include <algorithm>
#include <climits>
using namespace std;
struct triple
{
	int a,b,c;
};
class Triangle //表示一般三角形的抽象类
{
protected:
	int left;//左边
	int right; //右边
	int bottom;//底边
public:
	Triangle(int a,int b,int c):left(a),right(b),bottom(c){} //构造函数
	virtual void rotate() = 0;//顺时针旋转120度的纯虚函数
	virtual int getOutEdge() = 0;//计算外边数据纯虚函数
	virtual bool check(Triangle*t) = 0;//检测与相邻三角形是否相邻边相等的纯虚函数
	virtual int getNeighbor() = 0;//计算相邻三角形相邻边的纯虚函数
};

class NormalTriangle:public Triangle //底边朝下的三角形抽象类
{
public:
	NormalTriangle(triple &t):Triangle(t.a, t.b, t.c){}//构造函数
	void rotate()//旋转虚函数的覆盖
	{
		int tmp = left;
		left = bottom;
		bottom =right;
		right = tmp;
	}
};

class InverseTriangle:public Triangle //底边朝上的三角形抽象类
{
public:
	InverseTriangle(triple &t):Triangle(t.a,t.c,t.b){}
	void rotate()
	{
		int tmp = bottom;
		bottom = left;
		left = right;
		right = tmp;
	}
};

class Triangle1:public NormalTriangle //放在棋盘中第1个格子里的三角形类
{
public:
	Triangle1(triple &t):NormalTriangle(t){}
	int getOutEdge(){ return left;} //覆盖计算外沿边的虚函数
	int getNeighbor(){ return right;}//覆盖计算相邻三角形相邻边虚函数
	bool check(Triangle *t){ return t->getNeighbor() == this->bottom;}//覆盖检测相邻三角形合法性虚函数
};

class Triangle3:public NormalTriangle //放在第3个格子里的三角形类
{
public:
	Triangle3(triple &t):NormalTriangle(t){}
	int getOutEdge(){return right;}
	int getNeighbor(){return bottom;}
	bool check(Triangle*t){return t->getNeighbor() == this->left;}
};

class Triangle5:public NormalTriangle //放在第5个格子里的三角形类
{
public:
	Triangle5(triple &t):NormalTriangle(t){}
	int getOutEdge(){return bottom;}
	int getNeighbor(){return left;}
	bool check(Triangle *t){return t->getNeighbor() == this->right;}
};

class Triangle2 : public InverseTriangle //放在第2个格子里的三角形类
{
public:
	Triangle2(triple &t):InverseTriangle(t){}
	int getOutEdge(){return bottom;}
	int getNeighbor(){return right;}
	bool check(Triangle *t){return t->getNeighbor() == this->left;}
};

class Triangle4:public InverseTriangle //放在第4个格子里的三角形类
{
public:
	int getOutEdge(){return right;}
	int getNeighbor(){return left;}
	bool check(Triangle *t){return t->getNeighbor() == this->bottom;}
	Triangle4(triple &t):InverseTriangle(t){}
};
class Triangle6 : public InverseTriangle //放在第6个格子里的三角形类
{
public:
	Triangle6(triple &t):InverseTriangle(t){}
	int getOutEdge(){return left;}
	int getNeighbor(){return bottom;}
	bool check(Triangle *t){return t->getNeighbor() == this->right;}
};

class Game //三角形游戏类
{
	triple *tr;
	Triangle *t[6];//表示环状棋盘的指针数组(用顶层抽象类指针实现多态性)
	int Max;//最大外缘边数值和
	int p[6];//表示三角形下标的数组
	void move();//得到三角形全排列
	void clear();//清理指针数组t
	void play(int k);//对排列好的6个三角形回溯合法的格局
public:
	Game(triple Tr[]);//构造函数
	int theTriangleGame();//计算合法格局最大外缘编之和
};

Game::Game(triple Tr[]):Max(INT_MIN)
{
	tr = Tr;
	for(int i =0 ;i<6;i++)
		p[i] = i;
}

void Game::move() //用下标的环状全排列确定三角形的环状全排列
{
	t[0] = new Triangle1(tr[0]); //首元素是确定不变的
    t[1] = new Triangle2(tr[p[1]]);
    t[2] = new Triangle3(tr[p[2]]);
    t[3] = new Triangle4(tr[p[3]]);
    t[4] = new Triangle5(tr[p[4]]);
    t[5] = new Triangle6(tr[p[5]]);
}
void Game::clear()//清理三角形指针数组t的空间
{
    delete (Triangle1 *)t[0];
    delete (Triangle2 *)t[1];
    delete (Triangle3 *)t[2];
    delete (Triangle4 *)t[3];
    delete (Triangle5 *)t[4];
    delete (Triangle6 *)t[5];
}

void Game::play(int k) //回溯寻求合法格局中第k个三角形的摆法探索
{
	if(k>5) //六个三角形都已排好
	{
		if(t[0]->check(t[5]))//得到合法格局
		{
			int sum = 0;
			for(int i = 0;i < 6;i++) //计算外缘边之和
			{
				sum += t[i]->getOutEdge();
			}
			if(sum > Max)//跟踪最大者
			{
				Max = sum;
			}
		}
		return;
	}
	for(int i = 1;i<=3;i++)
	{
		t[k]->rotate();//旋转120度
		if(k>0 && !t[k]->check(t[k - 1]))//与前一个格子中的三角形相邻边不吻合
			continue;//再旋转
		play(k + 1);//与前者相邻边吻合,则进一步探索第k+1个三角形的合法摆放
	}
}

int Game::theTriangleGame() //计算6个三角形的所有环状全排列
{
	do
	{
		move();
		play(0);
		clear();
	}
	while(next_permutation(p + 1,p+6));
	return Max;
}

int main()
{
	ifstream inputdata("inputdata.txt");
	ofstream outputdata("outputdata.txt");
	char ch = '*';
	while(ch!='$') //读取一个案例数据
	{
		triple tr[6];
		for(int i = 0;i < 6;i++)
			inputdata>>tr[i].a>>tr[i].b>>tr[i].c;
		Game g(tr);
		int max = g.theTriangleGame();//计算合法最大外缘值之和
		if(max>INT_MIN)
		{
			outputdata<<max<<endl;
			cout<<max<<endl;
		}
		else
		{
			outputdata<<"none"<<endl;
			cout<<"none"<<endl;
		}
		inputdata>>ch;
	}
	inputdata.close();
	outputdata.close();
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值