题目:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。
例如,在下面的3x4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用下画线标出)。但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。
这是一个利用回溯法解决典型题。
一、回溯法简介
回溯法可以看成蛮力法的升级版,它从解决问题的每一步的所有可能选项里系统地选择出一个可行的解决方案。回溯法非常适合由多个步骤组成的问题,并且每个步骤都有多个选项。当我们在某一步选择了其中一个选项时,就进入下一步,然后又面临新的选项。我们就这样重复选择,直至到达最终状态。
用回溯法解决的问题的所有选项可以形象地用树状结构表示。在某一步有n个可能的选项,那么该步骤可以看成是树状结构中的一个节点,每个选项看成树中节点连接线,经过这些连接线到达该节点的n个子节点。树的叶节点对应着终结状态。
如果在叶节点的状态满足题目的约束条件,那么我们找到了一个可行的解决方案。
如果在叶节点的状态不满足约束条件,那么只好回溯到它的上一个节点再尝试其他的选项。如果上一个节点所有可能的选项都已经试过,并且不能到达满足约束条件的终结状态,则再次回溯到上一个节点。如果所有节点的所有选项都已经尝试过仍然不能到达满足约束条件的终结状态,则该问题无解。
二、解题过程
本题用回溯法解决问题的过程如下:
由于矩阵的第一行第二个字母‘b’和路径“bfce”的第一个字符相等,我们就从这里开始分析。根据题目的要求,我们此时有3个选项,分别是向左到达字母‘a’、向右到达字母‘t’、向下到达字母‘f’。我们先尝试选项‘a’,由于此时不可能得到路径“bfce”,因此不得不回到节点‘b’尝试下一个选项‘t’。同样,经过节点‘t’也不可能得到路径“bfce”,因此再次回到节点‘b’尝试下一个选项‘f’。
在节点‘f’我们也有3个选项,向左、向右都能到达字母‘c’、向下到达字母‘d’。我们先选择向左到达字母‘c’,此时只有一个选择,即问下到达字母‘j’。由于此时的路径为" bfcj ",不满足题目的约束条件,我们只好回到上一个节点左边的节点‘c’。注意到左边的节点‘c’的所有选项都已经尝试过,因此只好再回溯到上一个节点‘f’并尝试它的下一个选项,即向右到达节点‘c’。
在右边的节点‘c’我们有两个选择,即向右到达节点‘s’,或者向下到达节点‘e’。由于经过节点‘s’不可能找到满足条件的路径,我们再选择节点‘e’,此时路径上的字母刚好组成字符串" bfce ",满足题目的约束条件,因此我们找到了符合要求的解决方案。
从‘b’开始的选项所组成的树状结构如下图所示:
通常回溯法算法适合用递归实现代码。当我们到达某一个节点时,尝试所有可能的选项并在满足条件的前提下递归地抵达下一个节点。
三、算法实现
首先,在矩阵中任选一个格子作为路径的起点。假设矩阵中某个格子的字符为ch,并且这个格子将对应于路径上的第i个字符。如果路径上的第i个字符不是ch,那么这个格子不可能处在路径上的第i个位置。如果路径上的第i个字符刚好是ch,那么到相邻的格子寻找路径上的第i+1个字符。除矩阵边界上的格子之外,其他格子都有4个相邻的格子。重复这个过程,直到路径上的所有字符都在矩阵中找到相应的位置。
由于回溯法的递归特性,路径可以被看成一个栈。当在矩阵中定位了路径中前n个字符的位置之后,在与第n个字符对应的格子的周围都没有找到第n+1个字符,这时候只好在路径上回到第n-1个字符,重新定位第n个字符。
由于路径不能重复进入矩阵的格子,所有还需要定义和字符矩阵大小一样的布尔值矩阵,用来标识路径是否已经进入了每个格子。
以下代码实现该回溯算法:
bool hasPath(char* matrix, int rows, int cols, char* str)//矩阵字符串,行数,列数,路径字符串
{
if (matrix == nullptr || rows < 1 || cols < 1 || str == nullptr)
return false;
//如果矩阵字符串为空或者行、列数小于1或者路径字符串为空,则返回false
bool* visited = new bool[rows * cols];//监视矩阵,确保每个格子不被重复进入
memset(visited, 0, rows * cols);//初始化函数,可以将一段连续内存初始化为某个值
//开辟一个与矩阵字符串同样大小的空间,将其全部初始化为0
int pathLength = 0;//路径字符串下标
for (int row = 0; row < rows; ++row)//遍历矩阵
{
for (int col = 0; col < cols; ++col)
{
if (hasPathCore(matrix, rows, cols, row, col, str,
pathLength, visited))//判断路径字符串是否匹配成功
{
return true;
}
}
}
}
bool hasPathCore(const char* matrix, int rows, int cols, int row, int col, const char* str, int& pathLength, bool *visited)
//矩阵字符串,行数,列数,当前行,当前列,路径字符串,路径字符串下标,监视矩阵
{
if (str[pathLength] == '\0')
return true;
//如果路径字符串在矩阵字符串中匹配完成,返回true
bool hasPath = false;
if (row >= 0 && row < rows && col >= 0 && col < cols && matrix[row * cols + col] == str[pathLength] && !visited[row * cols + col])
//如果当前位置均在矩阵中,并且矩阵中的当前位置的字符等于路径字符串当前下标的字符,且该位置第一次进入
{
++pathLength;//路径字符串下标++
visited[row * cols + col] = true;//标记当前位置已经进入过,并且匹配成功
hasPath = hasPathCore(matrix, rows, cols, row, col - 1, str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row - 1, col, str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row, col + 1, str, pathLength, visited)
|| hasPathCore(matrix, rows, cols, row + 1, col, str, pathLength, visited);
//判断当前位置的相邻格子是否可以进行匹配
if (!hasPath)
{
--pathLength;//如果该位置相邻格子均不能进行匹配,则路径字符串下标--,在其他位置重新开始匹配
visited[row * cols + col] = false;//标记当前位置已经进入过,并且匹配失败
}
}
return hasPath;
}
当矩阵中坐标为(row,col)的格子和路径字符串中下标为pathLength的字符一样时,从4个相邻的格子(row,col-1)、(row-1,col)、(row,col+1)、(row+1,col)中去定位路径字符串中下标为pathLength+1的字符。
如果4个相邻的格子都没有匹配字符串中下标为pathLength+1的字符,则表明当前路径字符串中下标为pathLength的字符在矩阵中的定位不正确,我们需要回到前一个字符(pathLength-1),然后重新定位。
一直重复这个过程,直到路径字符串上的所有字符都在矩阵中找到合适的位置(此时str[pathLength]=='\0')。
四、测试代码
void Test(const char* testName, const char* matrix, int rows, int cols, const char* str, bool expected)
{
if (testName != nullptr)
printf("%s begins: ", testName);
if (hasPath(matrix, rows, cols, str) == expected)
printf("Passed.\n");
else
printf("FAILED.\n");
}
//ABTG
//CFCS
//JDEH
//BFCE
void Test1()
{
const char* matrix = "ABTGCFCSJDEH";
const char* str = "BFCE";
Test("Test1", (const char*)matrix, 3, 4, str, true);
}
//ABCE
//SFCS
//ADEE
//SEE
void Test2()
{
const char* matrix = "ABCESFCSADEE";
const char* str = "SEE";
Test("Test2", (const char*)matrix, 3, 4, str, true);
}
//ABTG
//CFCS
//JDEH
//ABFB
void Test3()
{
const char* matrix = "ABTGCFCSJDEH";
const char* str = "ABFB";
Test("Test3", (const char*)matrix, 3, 4, str, false);
}
//ABCEHJIG
//SFCSLOPQ
//ADEEMNOE
//ADIDEJFM
//VCEIFGGS
//SLHECCEIDEJFGGFIE
void Test4()
{
const char* matrix = "ABCEHJIGSFCSLOPQADEEMNOEADIDEJFMVCEIFGGS";
const char* str = "SLHECCEIDEJFGGFIE";
Test("Test4", (const char*)matrix, 5, 8, str, true);
}
//ABCEHJIG
//SFCSLOPQ
//ADEEMNOE
//ADIDEJFM
//VCEIFGGS
//SGGFIECVAASABCEHJIGQEM
void Test5()
{
const char* matrix = "ABCEHJIGSFCSLOPQADEEMNOEADIDEJFMVCEIFGGS";
const char* str = "SGGFIECVAASABCEHJIGQEM";
Test("Test5", (const char*)matrix, 5, 8, str, true);
}
//ABCEHJIG
//SFCSLOPQ
//ADEEMNOE
//ADIDEJFM
//VCEIFGGS
//SGGFIECVAASABCEEJIGOEM
void Test6()
{
const char* matrix = "ABCEHJIGSFCSLOPQADEEMNOEADIDEJFMVCEIFGGS";
const char* str = "SGGFIECVAASABCEEJIGOEM";
Test("Test6", (const char*)matrix, 5, 8, str, false);
}
//ABCEHJIG
//SFCSLOPQ
//ADEEMNOE
//ADIDEJFM
//VCEIFGGS
//SGGFIECVAASABCEHJIGQEMS
void Test7()
{
const char* matrix = "ABCEHJIGSFCSLOPQADEEMNOEADIDEJFMVCEIFGGS";
const char* str = "SGGFIECVAASABCEHJIGQEMS";
Test("Test7", (const char*)matrix, 5, 8, str, false);
}
//AAAA
//AAAA
//AAAA
//AAAAAAAAAAAA
void Test8()
{
const char* matrix = "AAAAAAAAAAAA";
const char* str = "AAAAAAAAAAAA";
Test("Test8", (const char*)matrix, 3, 4, str, true);
}
//AAAA
//AAAA
//AAAA
//AAAAAAAAAAAAA
void Test9()
{
const char* matrix = "AAAAAAAAAAAA";
const char* str = "AAAAAAAAAAAAA";
Test("Test9", (const char*)matrix, 3, 4, str, false);
}
//A
//A
void Test10()
{
const char* matrix = "A";
const char* str = "A";
Test("Test10", (const char*)matrix, 1, 1, str, true);
}
//A
//B
void Test11()
{
const char* matrix = "A";
const char* str = "B";
Test("Test11", (const char*)matrix, 1, 1, str, false);
}
void Test12()
{
Test("Test12", nullptr, 0, 0, nullptr, false);
}
int main(int argc, char* argv[])
{
Test1();
Test2();
Test3();
Test4();
Test5();
Test6();
Test7();
Test8();
Test9();
Test10();
Test11();
Test12();
return 0;
}