1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| 我们被问到:“触屏密码”问题。在4x4点阵上,有多少种不同的密码?用户要求用C++实现状压DP。用户提供了思路:dp[num][1<<num]表示当前最后一个触点是num,访问过的点集状态为mask。写一个checker检查从num向其他状态转移是否合法,然后计算。需要给cpp代码。
首先我们需要理解规则:4x4网格,触摸序列,连接直线。如果直线经过未访问过的中间点,会自动加入序列并视为两段移动。但已访问过的点已经消失,不能作为中间点再次加入。例如,如果直线经过一个已访问的点,不会中断或加入。但经过未访问的点,则必须把它包括进来(视为中间触点)。这意味着,从一个点到另一个点的移动,只有当两点之间的所有中间点(按照某种离散直线)都已经被访问过,或者没有中间点,才是合法的。否则,如果存在未访问的中间点,移动是不允许的(因为你会先碰到未访问的中间点,从而实际序列会先到达那个点)。题中描述:“如果在手指沿直线移动时经过了一个中间触点,则应当将该中间触点加入序列并视为两段移动。”这暗示用户只能划直线,如果直线经过未访问的点,那么实际序列必然包含那个点。因此,有效的一次滑动,必须是两点之间的所有“中间触点”都已经被访问过,或者没有中间触点。否则用户不可能直接从一个点划到另一个点而不经过中间未访问点。换言之,给定当前访问过的点集(已消失),下一个可以到达的点,必须满足从当前点(last)到目标点(next)的直线段上,所有经过的网格点(不包括端点)都已经在访问集中。这类似于Android图案锁的规则。在经典Android图案锁中,1->9经过5,如果5未访问,则必须经过5,即实际序列为1->5->9。如果5已访问,则可以直接1->9。这正是问题描述。因此我们要计算所有可能的序列,长度至少为2,最多为16(在4x4网格中)。没有说密码最小长度?题干说“选择两个至多个不同触点组成的序列”,所以长度>=2。在3x3的例子中,389488是所有可能的密码数,包括所有长度2到9。我们需要计算4x4的总数。
状态:16个点,0~15。我们可以对每个mask,记录最后一个点last,以及方案数。初始:选择第一个点可以任选?密码是由序列组成,所以起始可以是任意点。初始状态:mask只有一个点,count=1。然后进行转移:对于每个mask,对于其中每一个可能的last(即mask中包含last),对于所有未在mask中的点next,检查从last到next是否合法(即直线上的所有中间点均已访问)。若合法,则下一个状态为mask | (1<<next),last变为next。因为中间点已访问,所以不会触发自动加入?注意,如果中间点都在mask中,则用户可以直接划到next,而不会经过未访问点。所以可以合法转移。最终所有长度>=2的mask的计数之和即为答案。但需要注意:中间点是否包括那些因为跨越而“经过”的点?我们需要定义4x4网格的坐标。通常,网格坐标可以设为(x,y),x,y从0到3。两点之间的直线是否经过整数网格点,可以用最大公约数判定。从(x1,y1)到(x2,y2)的线段,经过的中间格点:dx=x2-x1, dy=y2-y1,令g=gcd(|dx|,|dy|)。如果g>1,则线段经过g-1个整数格点(不包括端点)。这些点的坐标为(x1 + idx/g, y1 + idy/g),i=1..g-1。我们需要所有这些点都在mask中。如果g==1,没有中间点,总是合法。
所以转移规则:对于last=(x1,y1),next=(x2,y2),g=gcd(|x1-x2|,|y1-y2|)。若g==1,合法;否则,对于i=1..g-1,计算中间点mid = (x1 + i(x2-x1)/g, y1 + i(y2-y1)/g),若某个mid不在mask中,则非法;全部在mask中则合法。
问题:序列中的中间触点自动加入。这其实在转移中如何体现?假设我们从mask={1},last=1,尝试滑向9。按照3x3规则,1->9会经过5,如果5不在mask中,我们不能直接1->9;实际序列是先到5,再到9。这相当于我们在DP中必须先转移到5,然后再从5转移到9。如果我们允许直接1->9当5未访问,就会漏掉经过5的路径,且与现实不符。因此我们的DP规则完美匹配:只有当中间点都已访问时,才能直接连接。如果中间点有未访问的,这次移动实际上不可能发生(用户手指会先碰到中间点)。所以DP正确。
关于密码长度:题目没有明确最小长度。但“选择两个至多个不同触点”,所以长度至少为2。对3x3的验证:已知389488。我们可以在代码中顺手验证3x3的结果,如果一致,就说明方法正确。然后再算4x4。注意需要高精度吗?答案可能很大。16! 约为2e13,但实际有约束,可能数字会比较大。我们需要用64位整数?3x3结果是389488,在int范围内。4x4结果也许超过32位?我们可以用long long,甚至可能需要__int128或大数。但通常这些题目答案可能适合64位。2e13 < 9e18,long long足够。16! = 20922789888000 ~ 2e13,加上各种限制,总数一定小于16!,所以long long肯定够。
我们要写C++代码来计算。状压DP:dp[mask][last] 方案数。mask从1到(1<<16)-1。我们可以使用vector<vector<long long>> dp(1<<16, vector<long long>(16, 0))。但1<<16=65536,乘16是约1M个元素,每个8字节大概8MB,完全可以。我们也可以使用滚动数组或只保留上一层?因为我们每次添加一个点,mask的popcount递增,可以按popcount遍历。但直接对mask循环并内部对last循环,然后枚举next也可以。但要注意顺序:因为mask从小到大,当处理mask时,其所有子状态都已经计算完毕?由于我们只从mask转移到mask| (1<<next),这是严格增加的,所以只需从0到(1<<N)-1遍历mask。对于初始状态,popcount==1。我们要对所有popcount==1的mask设dp[mask][last] = 1 (last为该点)。
然后遍历mask从1到(1<<N)-1。对于每个mask,获取其中所有可能的last(即mask & (1<<last)非零)。若dp[mask][last]==0,跳过。枚举next不在mask中。检查合法。合法则 dp[mask | (1<<next)][next] += dp[mask][last]。
最后,对于所有popcount >= 2的mask,求和dp[mask][last]得到总密码数。
注意:序列长度计算?我们只需把popcount>=2的所有状态累加即可,因为每个密码就是序列,其终点last和访问集mask一一对应。
优化:预先计算所有点对之间的中间点列表。对于4x4网格,点编号0~15。我们可以把坐标映射:x = id % 4, y = id / 4。或者 x = id / 4, y = id % 4,没关系。中间点列表:vector<vector<vector<int>>> intermediates(16, vector<vector<int>>(16)); 对于每对(i, j),计算中间点列表(不包括i, j)。如果dx, dy的gcd g>1,则计算中间点。检查合法性时,只需遍历intermediates[i][j],如果所有点都在mask中即可。
我们先写代码验证3x3是否为389488。N=3,坐标范围0..2。3x3点阵标号:通常是1-9,我们可以映射为0-8,不影响计数。计算结果应该是389488。
如果3x3结果正确,再计算4x4。
关于密码长度:题目说“两个至多个不同触点”,所以长度>=2。求总数。
题外话:4x4的结果可能是一个已知的序列。我们可以只打印结果。但用户要求提供cpp代码实现。所以输出完整cpp程序,包含计算和输出。
边缘情况:中间点经过已访问点,规则说不会再次加入序列。也就是说,如果点已经被访问,手指划过它没事,不会重复添加到序列。所以可以允许跨越已访问点。我们的规则要求中间点都在mask中,所以允许跨越。正确。
另外,关于“触点被接触后就会从屏幕上消失,在此之后,既不能作为未来移动时的终点,也不会因为在移动时经过而被作为中间触点再次加入序列。”所以我们不能重复访问同一个点。mask已经确保不重复。如果在滑动中经过已经消失的点,不会加入序列,我们的转移允许(因为中间点在mask中)。正确。
因此DP没有问题。
现在编写代码。我们将N设为4(或先用3验证)。我们可以实现一个函数 solve(int N),返回总密码数。主函数调用并输出。
注意:对于4x4,总状态数1<<16 = 65536,dp大小65536*16=1,048,576,long long完全OK。计算时间也不长。可以使用嵌套循环。
为提高速度,预处理:
坐标数组:x[i], y[i]。
中间点数组:mid[i][j] = 中间点索引列表。
预先计算每个mask的popcount?(可选,可以用__builtin_popcount,或者直接判断mask中点的数量,但最终我们累加popcount>=2的项)。
我们还可以预先计算所有(mask, i, j)的合法性,但那样太多(65536*16*16=16M),可以即时计算:对于last和next,遍历mid[last][next],检查mask中是否包含全部。检查可以是 (mask & mid_mask) == mid_mask,其中mid_mask是中间点集合的位掩码。我们可以预处理中间点掩码mid_mask[i][j](一个整数位掩码)。这样合法性检查变为 (mask & mid_mask[i][j]) == mid_mask[i][j]。速度极快。
预处理mid_mask: 对于i, j,如果i==j,则0。如果g=1,为0。否则计算mid_mask。
然后DP循环: 初始化 dp[1<<i][i] = 1 对于所有i。
for mask from 1 to (1<<N)-1: for last in 0..N-1: if !(mask & (1<<last)) continue; cnt = dp[mask][last]; if cnt == 0 continue; for next in 0..N-1: if mask & (1<<next) continue; if (mask & mid_mask[last][next]) != mid_mask[last][next] continue; dp[mask | (1<<next)][next] += cnt;
最终求和: ans = 0; for mask from 1 to (1<<N)-1: if popcount(mask) >= 2: for last in 0..N-1: if mask & (1<<last): ans += dp[mask][last];
打印ans。
现在验证3x3: N=3, 1<<3 = 8? N=3是1<<9=512。哦,3x3点阵有9个点。N=9。网格尺寸是size=3,N=size*size。对于4x4,size=4,N=16。
所以代码参数是 size=4,N=size*size=16。先测试size=3,N=9,看结果是否389488。
另外,我们需要注意dp数组可能较大,但可以只开一个二维vector,或者使用一维vector<vector<long long>>。内存:dp[512][9]很小。dp[65536][16] 约8MB。没问题。
编写代码,加上必要的头文件。
输出答案:计算并打印。
此外,关于密码的最小长度,题目说“选择两个至多个不同触点”,所以长度2到N。我们的求和popcount>=2正确。
现在写代码。注意dp[mask][last]可能会很大,用long long。如果担心溢出,可以用unsigned long long,但389488在int范围内,4x4应该也在long long范围内。我们还可以输出结果看看。
最终只需要提供cpp代码。我会附上简短解释。
|