本课程是从少年编程网转载的课程,目标是向中学生详细介绍计算机比赛涉及的编程语言,数据结构和算法。编程学习最好使用计算机,请登陆 www.3dian14.org (免费注册,免费学习)。
我们知道,通过手机上的键盘按键(不管是之前手机上的物理键盘还是现在触摸屏上的虚拟键盘),可以按出相应的电话号码。
我们来看一个有趣的问题:
假设您只能按键盘上的数字键(0-9),而不允许按底行的角按钮(即*和#)。这样,您可以按出只有数字0-9组成的数字串。
再假设您按了某个数字键之后,接着只能继续按当前数字按键或者位于当前数字键的上,左,右或下的数字键。也就是下一个按键是有限制的,不是任意选择的。
例如,你按了数字5以后,下次只能按数字5,2,4,6,或8。按了数字0以后,下次只能再按数字0或8。
好,现在来看今天的问题,在上述限制条件下,请找出您能按出的长度为N的数字串的数量。
我们来看些例子:
当N = 1,您可以按0,1,2,...,9,也就是可能的数字串数量为10个。
对于N = 2,可能的数字是00,08 11,12,14 22,21,23,25等等。我们具体看一下:
如果我们以0开头,则有效数字串将为00、08(计数:2)
如果我们从1开始,有效数字串将是11、12、14(计数:3)
如果我们从2开始,有效数字串将是22、21、23.25(计数:4)
如果我们以3开头,则有效数字串将为33、32、36(计数:3)
如果我们从4开始,有效数字串将是44,41,45,47(计数:4)
如果我们从5开始,有效数字串将是55、54、52、56、58(计数:5)
如果我们从6开始,有效数字串将是66,63、65、69(计数:4)
如果我们从7开始,有效数字串将是77,74、78(计数:3)
如果我们从8开始,有效数字串将是88,85、87、89、80(计数:5)
如果我们从9开始,有效数字串将是99,96、98(计数:3)
因此总数为2+3+4+3+4+5+4+3+5+3=36。
算法分析
在上述限制条件下,对于给定的N,我们需要统计所有长度为N的数字串的个数。
1)N = 1是最简单的情况,可能的数量为10(0,1,2,3,…,9)
2)对于N>1,我们需要从某个按键开始,然后重复选择按键或移至四个方向(向上,向左,向右或向下)中的任意一个,然后转到一个有效按键(不应转到*,#)。继续执行此操作,直到获得N个长度数字(深度优先遍历)。
递归是很顺理成章的解决方案。
我们把键盘看成4X3的矩形网格(4行3列)
假设Count(i,j,N)代表从位置(i,j)开始的长度为N的数字串的个数
1)如果N = 1
Count(i,j,N)= 10
2)其他情况:
Count(i,j,N)= 所有Count(r,c,N-1)的总和,其中(r,c)是 新的从当前位置有效移动长度1之后的新位置(i,j)
朴素递归算法
以下是上述递归公式的实现例子。
//朴素递归算法
#include
//从当前位置移动到左、上、右、下的位置
int row[] = {0, 0, -1, 0, 1};
int col[] = {0, -1, 0, 1, 0};
// 返回从位置(i,j)开始的长度为N的数字串的个数
int getCountUtil(char keypad[][3], int i, int j, int n)
{
if (keypad == NULL || n <= 0)
return 0;
//对于当前键,长度为1只有一种可能
if (n == 1)
return 1;
int k=0, move=0, ro=0, co=0, totalCount = 0;
//从当前位置向左,向上,向右,向下移动
//如果新位置有效,从该新位置开始,计算长度为(n-1)的数字串个数,并加上到目前为止获得的计数
for (move=0; move<5; move++)
{
ro = i + row[move];
co = j + col[move];
if (ro >= 0 && ro <= 3 && co >=0 && co <= 2 &&
keypad[ro][co] != '*' && keypad[ro][co] != '#')
{
totalCount += getCountUtil(keypad, ro, co, n-1);
}
}
return totalCount;
}
//返回长度为n的所有可能数字串的计数
int getCount(char keypad[][3], int n)
{
// 递归基础
if (keypad == NULL || n <= 0)
return 0;
if (n == 1)
return 10;
int i=0, j=0, totalCount = 0;
for (i=0; i<4; i++) //行循环
{
for (j=0; j<3; j++) // 列循环
{
// Process for 0 to 9 digits
if (keypad[i][j] != '*' && keypad[i][j] != '#')
{
// Get count when number is starting from key
// position (i, j) and add in count obtained so far
//计算从位置(i,j)的键开始的数字串个数,并添加到目前为止获得的计数
totalCount += getCountUtil(keypad, i, j, n);
}
}
}
return totalCount;
}
// 主程序
int main(int argc, char *argv[])
{
char keypad[4][3] = {{'1','2','3'},
{'4','5','6'},
{'7','8','9'},
{'*','0','#'}};
printf("Count for numbers of length %d: %dn", 1, getCount(keypad, 1));
printf("Count for numbers of length %d: %dn", 2, getCount(keypad, 2));
printf("Count for numbers of length %d: %dn", 3, getCount(keypad, 3));
printf("Count for numbers of length %d: %dn", 4, getCount(keypad, 4));
printf("Count for numbers of length %d: %dn", 5, getCount(keypad, 5));
return 0;
}
动态规划优化
我们可以看到,在上述递归算法中,较长的遍历路径上有很多是对较短的遍历路径的重复计算。例如:
例如,请参见以下两个图。
下图列出从数字8开始的长度为4的部分数字串。
而下图列出从数字5开始的长度为4的部分数字串。
从图中可以看到有些重复的遍历(例如4-> 1,6-> 3,8-> 9、8-> 7等等)。该问题明显具有以下两个属性:最佳子结构和重叠子问题,因此可以使用动态编程有效地解决。
下面是用动态规划算法优化后的一种实现例子。请大家运行这个程序,并体会是如何保存中间结果的。
//动态规划算法
#include
// 返回从位置(i,j)开始的长度为N的数字串的个数
int getCount(char keypad[][3], int n)
{
if(keypad == NULL || n <= 0)
return 0;
if(n == 1)
return 10;
//从当前位置移动到左、上、右、下的位置
int row[] = {0, 0, -1, 0, 1};
int col[] = {0, -1, 0, 1, 0};
//为简单起见,取count [i] [j]将存储
//以数字i开头的长度为j的数字串个数
int count[10][n+1];
int i=0, j=0, k=0, move=0, ro=0, co=0, num = 0;
int nextNum=0, totalCount = 0;
//以数字i开头且长度为0和1的数字
for (i=0; i<=9; i++)
{
count[i][0] = 0;
count[i][1] = 1;
}
//自下而上,计算长度为2、3、4,...,n的数字串个数
for (k=2; k<=n; k++)
{
for (i=0; i<4; i++) // 键盘行循环
{
for (j=0; j<3; j++) // 键盘列循环
{
// 处理数字0-9
if (keypad[i][j] != '*' && keypad[i][j] != '#')
{
//统计数字keypad[i][j]开头的并且长度为k的数字串个数,数字keypad[i][j]
//是第一个数字,我们需要再找(k-1)个数字
num = keypad[i][j] - '0';
count[num][k] = 0;
//从当前位置向左,向上,向右,向下移动
//如果新位置有效,从该新位置开始,计算长度为(n-1)的数字串个数,并加上到目前为止获得的计数
for (move=0; move<5; move++)
{
ro = i + row[move];
co = j + col[move];
if (ro >= 0 && ro <= 3 && co >=0 && co <= 2 &&
keypad[ro][co] != '*' && keypad[ro][co] != '#')
{
nextNum = keypad[ro][co] - '0';
count[num][k] += count[nextNum][k-1];
}
}
}
}
}
}
//累加计算所有长度为 n从数字0,1,2,...,9起始的数字串个数
totalCount = 0;
for (i=0; i<=9; i++)
totalCount += count[i][n];
return totalCount;
}
// 主程序
int main(int argc, char *argv[])
{
char keypad[4][3] = {{'1','2','3'},
{'4','5','6'},
{'7','8','9'},
{'*','0','#'}};
printf("Count for numbers of length %d: %dn", 1, getCount(keypad, 1));
printf("Count for numbers of length %d: %dn", 2, getCount(keypad, 2));
printf("Count for numbers of length %d: %dn", 3, getCount(keypad, 3));
printf("Count for numbers of length %d: %dn", 4, getCount(keypad, 4));
printf("Count for numbers of length %d: %dn", 5, getCount(keypad, 5));
return 0;
}
思考题
上述动态编程方法也需要O(n)时间运行,因为只有一个for循环运行n次,其他for循环需运行恒定时间。
但是上述算法需要O(n)辅助空间存放临时数据。事实上可以看到,第n次迭代仅需要第(n-1)次迭代中的数据,因此我们不需要保留较旧迭代中的数据,可以使用只有两个大小为10的数组的动态编程方法来实现,请你试试看。