问题描述:
给定一个m行n列的矩阵,从左上角开始每次只能向右或向下移动,最后到达右下角的位置,路径上的所有数字累加起来作为这条路径的路径和。编写程序求所有路径和中的最小路径和。例如,一下矩阵中的路径1->3->1->0->6->1->0是所有路径中路径和最小的,结果为12.
1 3 5 9
8 1 3 4
5 0 6 1
8 8 4 0
要求:
1)使用随机数算法生成一个行数和列数均大于10000的矩阵,其中随机数范围为[0,100],相邻数字差距大于10,每个数字出现频率相近(考察报告中部分截图显示即可);
2)基于搜索算法(蛮力法、回溯法、分支限界法)设计一个程序解决该问题;
随机数算法
使用rand和srand生成随机数,由于rand() 函数产生的随机数是伪随机数,是根据一个数值按照某个公式推算出来的,这个数值称之为“种子”。种子和随机数之间的关系是一种正态分布,通过 srand() 函数来重新“播种”,这样种子就会发生改变,使用时间作为参数,只要每次播种的时间不同,那么生成的种子就不同,最终的随机数也就不同。使用rand() % (b - a + 1) + a生成[a,b]的随机数,传入0和100可生成[0,100]的随机数,使用last存放上一次的随机数的结果,即矩阵前一个数的结果,使用now存放当前随机数,使用while循环判断直到前一个和当前值大于10,给now赋予新值并添加到矩阵中。这样就可以使矩阵相邻的数差距大于10.
分支限界算法
首先想无论是使用蛮力法还是其他的方法都是先将题目做出来,由于输入是一个矩阵,使用数组存放,假设有M行N列,路径要从左上角走到右下角,而且每次只能向右走或者向下走,仔细观察发现无论用哪种方法向下走或向右走的步数永远分别是M,N减一,一共要走的步数就是M+N-2。那么可以想象他的解空间树,他是一个二叉树,只有向右走或者向下走,假设左子树是向下走,右子树是向右走,解空间树的高度就是M+N-2。解空间树中每个结点存放当前所在行与当前所在列,当前结点的总路径长度和解向量,用FIFO表示队列(实际上对应层次遍历),初始时,FIFO=[ ],每遍历一层将其入队列。
采用STL的queue容器qu作为队列,队列中的结点类型声明如下:
现在设计限界函数,为了简便,设根结点为第0层,然后各层依次递增,当结点所在行数和列数相加等于M+N-2时表示是叶子结点层。解空间树如下
由于该问题是求路径和最小,属求最小值问题,采用下界设计方式,假设整体的下界为smallest,初始的smallest设置的初始路径是矩阵最上面一行和最右边一列。对于第i层的某个结点e,用(e.row,e.column)表示结点e的位置,用e.length表示走到当前结点的路径和,那么当前结点的路径和为:
e1.length = e.length + Arr[e.column][e.row + 1],那么此时价值的下界为e1.length,即后面的结点都不选可达到的路径和,如果此时的下界超过了smallest,则进行减枝。如果路径向右或者向下走的步数超过了行数或者列数,同样进行减枝。
路径向下走的剪枝
路径向右走进行剪枝
核心代码,使用广度优先搜索求最短路径:
结点进队并判断是否为叶子结点,如果是叶子结点,判断此时的路径是否为最短路径,如果比最短路径还要短则将此时的路径赋值给最短路径,并将当前结点的解向量赋值给最优解向量,用于输出。
实验结果与分析:
测试数据由(1)随机生成的,矩阵大小为18*18,其中随机数范围为[0,100],相邻数字差距大于10,每个数字出现频率相近,一共运行三分钟,此程序的时间复杂度为logM+N。
本机配置,CPU:i7-8750,内存:16G,当程序运行时候打开任务管理器发现,内存几乎跑满,但是CPU只有一小部分的消耗,说明此程序调用内存,消耗内存更多,但是运算更少,现在对老师说过的递归调用函数消耗内存更大有了更深刻的理解。
再次测试10*10的矩阵观察输出,当输入矩阵,输出路径和最短路径。
程序源码
#include <iostream>
#include <cstdlib>
#include <ctime>
#include <time.h>
#include <queue>
#define M 18
#define N 18
int Arr[M][N] = {{0}};
int x[M + N - 1] = {0}; //最优解向量
int smallest = 0; //最短路径和
using namespace std;
//队列结点类型
struct NodeType //队列结点类型
{
int row; //所在行号
int column; //所在列号
int length; //当前结点总路径长度
int thisx[M + N - 1] = {0}; //当前结点包含的解向量
};
//生成随机数
void random(int a, int b)
{
int now = 0, last = 0;
srand((int)time(0)); //
for (int i = 0; i < M; i++)
{
for (int j = 0; j < N; j++)
{
while (-11 < last - now && last - now < 11)
{
now = rand() % (b - a + 1) + a;
}
Arr[i][j] = now;
last = now;
}
}
}
void EnQueue(NodeType e, queue<NodeType> &qu)
//结点e进队qu
{
if (e.row + e.column == M + N - 2) //到达叶子结点
{
if (e.length < smallest) //找到路径最短的解
{
smallest = e.length;
for (int j = 0; j < M + N - 1; j++)
x[j] = e.thisx[j];
}
}
else
qu.push(e); //非叶子结点进队
}
void bfs()
{
//给最短路径设置初始值,假设他一直向右走再一直向下走
for (int i = 0; i < M; i++)
{
smallest += Arr[0][i];
}
for (int i = 1; i < N; i++)
{
smallest += Arr[i][M - 1];
}
NodeType e, e1, e2;
queue<NodeType> qu;
e.row = 0;
e.column = 0; //建立源点结点e(根结点)
e.length = Arr[e.row][e.column];
e.thisx[0] = e.length;
qu.push(e); //源点结点e进队
while (!qu.empty()) //队列不空循环
{
e = qu.front();
//cout<<e.column<<" "<<e.row<<" "<<e.length<<endl;
qu.pop(); //出队列结点e
if (e.length + Arr[e.column][e.row + 1] < smallest && e.row < M - 1) //向下走,检查下界并进行剪枝
{
e1.row = e.row + 1;
e1.column = e.column;
e1.length = e.length + Arr[e1.column][e1.row];
for (int i = 0; i < e1.row + e1.column; i++) //复制解向量
{
e1.thisx[i] = e.thisx[i];
}
e1.thisx[e1.row + e1.column] = Arr[e1.column][e1.row];
EnQueue(e1, qu); //向右走的结点进队操作
}
if (e.length + Arr[e.column + 1][e.row] < smallest && e.column < N - 1) //向右走,检查下界并进行剪枝
{
e2.row = e.row;
e2.column = e.column + 1;
e2.length = e.length + Arr[e2.column][e2.row];
for (int i = 0; i < e2.row + e2.column; i++)
e2.thisx[i] = e.thisx[i];
e2.thisx[e2.row + e2.column] = Arr[e2.column][e2.row];
EnQueue(e2, qu); //向右走的结点进队操作
}
}
}
int main()
{
random(0, 100);
for (int i = 0; i < M; i++)
{
for (int j = 0; j < N; j++)
cout << Arr[i][j] << " ";
cout << endl;
}
clock_t start, finish; //clock_t为CPU时钟计时单元数
start = clock();//clock()函数返回此时CPU时钟计时单元数
bfs();
cout << "解向量为";
for (int i = 0; i < M + N - 1; i++)
{
cout << x[i] << "->";
}
cout << "最短路径和为" << smallest << endl;
finish = clock(); //clock()函数返回此时CPU时钟计时单元数
cout << "共耗时:" << double(finish - start) / CLOCKS_PER_SEC << endl; //finish与start的差值即为程序运行花费的CPU时钟单元数量,再除每秒CPU有多少个时钟单元,即为程序耗时
return 0;
}