程序员的算法趣题2
上QQ阅读APP看书,第一时间看更新

Q 02 山手线的印章比赛

IQ 70
目标时间:15 分钟

我们来看一下山手线的印章比赛。假设所有车站都有印章,参与者必须在进站的车站和出站的车站盖章。印章在检票口里面,进站后可以在不出站的情况下收集印章。

山手线是东京都内的一条环状地铁线,一共有 29 个车站。这里假设不能通过山手线换乘其他路线,并且只能在山手线单向通行。参与者购买单程车票,从一个车站到另一个车站,每个车站只能通过一次(中途逆行就算违反规则)。

假设印章卡上能够盖所有车站的印章,且山手线上的各站用 1 ~ 29 按顺序编了号。

问题

如果从 1 号车站进入,从 17 号车站出站,则一共有多少种盖章方式(图 1.3)?

图 1.3 问题的示意图

1山手线是环形运行的,靠内侧运行的叫作内环,在其外侧反向运行的叫作外环。我国是右侧通行,所以内环是顺时针,外环是逆时针,而日本是左侧通行,所以内外环运行的方向与我国的情况恰好相反。——编者注

思路

为了简化问题,我们可以把车站想象成笔直的一列,而不是一个圆环。由于不能逆行,所以我们只要考虑参与者在各站是否下车即可,由此便能求出组合数。

比如,在有 1、2、3、4、5 这 5 个车站的情况下,组合方式一共有 8 种,具体如图 1.4 所示。

图 1.4 有 5 个车站的情况

因为我们一定会在进站和出站的两个车站盖章,所以只要思考是否在 2 号、3 号和 4 号车站下车即可。用就能求出结果。也就是说,如果“中间的车站数”有个,组合方式就有种。

代码清单 02.01q02.rb

N = 29

# 设置进站和出站的车站号
a, b = 1, 17

# 求中间经过的车站数
n = (a - b).abs

# 将内环和外环的情况相加后,去掉重复的部分
puts (1 << (n - 1)) + (1 << (N - n - 1)) - 1

代码清单 02.02q02.js

N = 29;

var a = 1;
var b = 17;
var n = Math.abs(a - b);

console.log((1 << (n - 1)) + (1 << (N - n - 1)) - 1);

关键点

在求时,用到了移位运算符“<<”。“<<”是左移运算符,1<<3 表示将二进制数 1 左移 3 位,右侧的空位补零(图 1.5)。

图 1.5 左移运算符

左移 1 位,表示的数就会变成原数的 2 倍,右移 1 位,则会缩小为原数的 1/2,所以要计算 2 的次方,只要将二进制数 1 往左移次就可以了。

在计算次方时,在使用 Ruby 的情况下可以写成 a ** b,在使用 JavaScript 的情况下可以写成 Math.pow(a, b),但如果底数是 2,即如果求的是 2 的乘方,那么使用移位运算符的处理速度会更快。

※根据 ES2016,在使用 JavaScript 的情况下也能写成 a ** b 的形式。

答案 36 863 种

前辈的小讲堂 实用的位运算

除了在本题中用到的移位运算,ANDORXOR 等位运算(逻辑运算)也很常用。位运算不仅能让代码变得简洁,提高处理速度,还能在一份数据中融入多条信息。

使用位运算容易给人一种“技术狂”的印象,但其实在实际工作中我们经常会用到它。例如,在开发 Windows 应用程序时,我们经常用位运算判断文件和文件夹的属性。

已经保存的文件会被赋予“只读”“隐藏”等各种属性。这部分是通过 FileAttributes 枚举来管理的。各类属性使用 1, 2, 4, 8, 16,…这种 2 的乘方的枚举常量来定义。表 1.4 中列出了这些属性,各个属性按比特位赋特定的值。

表 1.4 FileAttributes 的属性

例如,我们可以按照下面的方式使用 AND 来判断文件是否设置了只读位。

attr = File.GetAttributes("文件名")
if (attr & FileAttributes.ReadOnly) == FileAttributes.ReadOnly