user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

触屏密码
触屏设备的解锁“密码”是由用户从屏幕上的长方形点阵中选择两个至多个不同触点组成的序列。用户先触摸第一个触点,然后沿直线移动到第二个触点,依此类推直至完成序列。在这过程中,用户的手指必须始终接触屏幕,且只能沿直线移动。

如果在手指沿直线移动时经过了一个中间触点,则应当将该中间触点加入序列并视为两段移动。例如,考虑一个大小为3*3且标有数字1至9的点阵(如下所示),序列1-9实际上应当视为序列1-5-9。

触点被接触后就会从屏幕上消失,在此之后,既不能作为未来移动时的终点,也不会因为在移动时经过而被作为中间触点再次加入序列。例如,序列1-9-3-7(期间会经过触点5两次)实际上应当视为序列1-5-9-6-3-7。

在3*3的点阵上,一共有389488种不同的密码。

求在4*4的点阵上,一共有多少种不同的密码?

我的思路:状压dp.
设 dp[num][2**num] 表示当前解锁的头是num,同时整个屏幕有哪些点已经被遍历过的状态为2**num,写一个checker检查从num往其他状态转移是否合法,然后直接计算即可.
请给我cpp代码实现.

deepseek 思考

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代码。我会附上简短解释。

以下是用状态压缩DP求解4x4触屏密码总数的C++代码,核心思路与你所述一致:

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
#include <iostream>
#include <vector>
#include <cstdint>
using namespace std;

int main() {
const int N = 16; // 4x4 网格共 16 个点
const int TOTAL_STATES = 1 << N;

// 坐标:点 id -> (x, y),x 为行,y 为列
int x[16], y[16];
for (int i = 0; i < 16; ++i) {
x[i] = i / 4;
y[i] = i % 4;
}

// 预处理两点之间的中间点掩码
// mid_mask[i][j] 的二进制位表示从 i 直线滑动到 j 时必须经过的所有中间点
vector<vector<int>> mid_mask(16, vector<int>(16, 0));
for (int i = 0; i < 16; ++i) {
for (int j = 0; j < 16; ++j) {
if (i == j) continue;
int dx = x[j] - x[i];
int dy = y[j] - y[i];
int g = gcd(abs(dx), abs(dy));
if (g > 1) {
int step_x = dx / g;
int step_y = dy / g;
int mask = 0;
for (int k = 1; k < g; ++k) {
int mid_x = x[i] + k * step_x;
int mid_y = y[i] + k * step_y;
int mid_id = mid_x * 4 + mid_y;
mask |= (1 << mid_id);
}
mid_mask[i][j] = mask;
}
}
}

// dp[mask][last]:当前已访问点集为 mask,且最后一个访问的点是 last 的方案数
vector<vector<long long>> dp(TOTAL_STATES, vector<long long>(16, 0));

// 初始状态:只选择一个点
for (int i = 0; i < 16; ++i) {
dp[1 << i][i] = 1;
}

// 状压 DP
for (int mask = 1; mask < TOTAL_STATES; ++mask) {
// 只对 popcount >= 1 的状态继续扩展
for (int last = 0; last < 16; ++last) {
if (!(mask & (1 << last))) continue; // last 必须在 mask 中
long long cur = dp[mask][last];
if (cur == 0) continue;

for (int nxt = 0; nxt < 16; ++nxt) {
if (mask & (1 << nxt)) continue; // nxt 未被访问
// 检查从 last 到 nxt 直线上的所有中间点是否都已访问
if ((mask & mid_mask[last][nxt]) != mid_mask[last][nxt]) continue;
dp[mask | (1 << nxt)][nxt] += cur;
}
}
}

// 统计所有长度 >= 2 的密码序列(即 mask 中至少包含两个点)
long long ans = 0;
for (int mask = 1; mask < TOTAL_STATES; ++mask) {
if (__builtin_popcount(mask) < 2) continue; // 密码长度至少为 2
for (int last = 0; last < 16; ++last) {
if (mask & (1 << last)) {
ans += dp[mask][last];
}
}
}

cout << "4x4 点阵的不同密码总数: " << ans << endl;
return 0;
}

算法说明:

  • 状态定义dp[mask][last] 表示当前屏幕上已经消失的触点集合为 mask,且最后一次触摸的触点为 last 的合法序列数。
  • 初始化:序列可以从任意点开始,因此对所有单点状态 dp[1<<i][i] = 1
  • 转移条件:从当前点 last 划向一个新点 nxt 时,只有当 lastnxt 直线段上的所有中间触点都已存在于 mask 中(即已经被访问过)时,这次移动才合法。这是因为如果中间有未访问的点,手指必然会先碰到它,从而破坏直接连接的意图。
  • 中间点判断:通过坐标差值的最大公约数 gcd(|dx|, |dy|) 判定。若 gcd > 1,则直线上存在中间网格点,提前用位掩码 mid_mask[i][j] 记录这些点。
  • 统计答案:题目要求选择“两个至多个不同触点”,因此将所有 popcount(mask) >= 2 的状态方案数累加,即得总密码数。

此代码可以直接编译运行,计算出 4x4 点阵的不同密码总数。

fun fact

也是一发过.