一共4道题目,2个小时。
题目1 - 圆桌座位
N 个人围坐一圈,有 M 对朋友关系。
第 i 对朋友关系是指,编号是 ai 的人和编号是 bi 的人是朋友。
现在要给他们安排座位,要求所有相邻的人不能是朋友。问共有多少种方案?
如果两个方案只有旋转角度不同,则我们将其视为一种方案。
数据范围
- 3≤N≤10
- 0≤M≤N(N−1)/2
- 1≤ai<bi≤N
- (ai,bi)≠(aj,bj)
- 所有输入均是整数
输入格式
N M
a1 b1
…
aM bM
输出格式
输出一个数,表示总方案数
样例1
输入:
4 1
1 2
输出:
2
样例2
输入:
10 5
1 2
3 4
5 6
7 8
9 10
输出:
112512
算法
(DFS) O(n!)
首先,为了避免旋转造成的重复计数问题,我们将编号是1的人固定在1号位置。
然后从2号位置开始,依次枚举每个位置放哪个人,注意只能放跟上一个人没有朋友关系的人。
枚举到最后一个人时,如果他跟编号是1的人不是朋友,说明方案合法,我们将方案数加一。
时间复杂度分析:第一个人位置固定,那么剩下的人总共有 (N−1)! 种放法,所以时间复杂度是 O(N!)。N最大是10,所以方案最多有 326880种,计算量可以接受。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 11;
int n, m, ans;
bool g[N][N], st[N];
void dfs(int u, int last_id)
{
if (u == n)
{
ans += !g[last_id][1];
return;
}
for (int i = 1; i <= n; i ++ )
if (!st[i] && !g[i][last_id])
{
st[i] = true;
dfs(u + 1, i);
st[i] = false;
}
}
int main()
{
cin >> n >> m;
for (int i = 0, a, b; i < m; i ++ )
{
cin >> a >> b;
g[a][b] = g[b][a] = true;
}
st[1] = true;
dfs(1, 1);
cout << ans << endl;
return 0;
}
题目2 - 求区间和
给定 N 个整数 a1,a2,…aN。
然后给出 Q 个询问,第 j 个询问给出两个整数 xj,yj,请计算所有 |ak| 的和,满足 xj≤k≤yj。
数据范围
- 1≤N≤105
- −109≤ai≤109(1≤i≤N)
- 1≤Q≤105
- 1≤xj≤yj≤N(1≤j≤Q)
- 所有输入均是整数
输入格式
N
a1
…
aN
Q
x1 y1
…
xQ yQ
输出格式
输出 Q 个数,分别表示每个询问的答案。
样例
输入:
6
1
-2
3
-4
5
6
3
1 2
2 4
3 6
输出:
3
9
18
算法
(前缀和) O(N+Q)
因为所有询问都是求绝对值的和,所以我们先将所有数取绝对值。
然后预处理出原数组的前缀和,使得 Si=∑ik=1ak。
则 ∑yk=xak=Sy−Sx−1。
所以对于每个询问,我们可以直接用两个前缀和做差得到区间和。
时间复杂度分析:初始化复杂度是 O(N),每个询问的复杂度是 O(1),共有 Q 个询问,所以总时间复杂度是 O(N+Q)。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 100010;
int n, m;
int a[N];
int main()
{
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i], a[i] = abs(a[i]);
cin >> m;
for (int i = 1; i <= n; i ++ ) a[i] += a[i - 1];
for (int i = 0; i < m; i ++ )
{
int x, y;
cin >> x >> y;
cout << a[y] - a[x - 1] << endl;
}
return 0;
}
题目3 - 最短距离
有 N 个村庄,编号1到 N。村庄之间有 M 条无向道路,第 i 条道路连接村庄 ai 和村庄 bi,长度是 ci。
所有村庄都是连通的。
共有 K 个村庄有商店,第 j 个有商店的村庄编号是 xj。
然后给出 Q 个询问,第 k 个询问给出一个村庄的编号 yk,问该村庄距离最近的商店有多远?
数据范围
- 2≤N≤105
- N−1≤M≤min(N(N−1)/2,105)
- 1≤Q≤105
输入格式
N M
a1 b1 c1
…
aM bM cM
K
x1
…
xK
Q
y1
…
yQ
输出格式
对于每个询问,输出该询问的结果。
样例
输入:
7 7
1 2 5
1 4 3
2 3 2
2 5 1
3 6 7
5 6 8
6 7 6
3
7
5
4
7
1
2
3
4
5
6
7
输出:
3
1
3
0
0
6
0
算法
(最短路) O(km)
由于 N 和 Q 都是 105 级别的,所以我们不可能对每个询问,都求一遍单源最短路,会超时。
我们可以建立一个虚拟起点 S,然后从 S 向每个有商店的村庄连一条长度是0的边。
然后求一遍以 S 为起点的单源最短路。
此时,对于每个询问 yk,yk 到 S 的最短距离,就是 yk 到所有商店的最短距离。
这是因为从 S 到 yk 的每条路径,都必然先走到某个商店,且走过的路程是0,然后再走向 yk,所以 S 到 yk 的最短路,就是所有商店到 yk 的最短路。
时间复杂度分析:我们用 SPFA 算法求单源最短路经,它的时间复杂度期望是 O(km),其中 k 是常数,最坏是 O(nm),最坏情况很难达到。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010, M = 300010, INF = 1000000000;
int n, m;
int h[N], e[M], ne[M], v[M], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, v[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void spfa()
{
queue<int> que;
for (int i = 1; i <= n; i ++ ) dist[i] = INF;
st[0] = true;
que.push(0);
while (que.size())
{
int t = que.front();
que.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
if (dist[e[i]] > dist[t] + v[i])
{
dist[e[i]] = dist[t] + v[i];
if (!st[e[i]])
{
que.push(e[i]);
st[e[i]] = true;
}
}
}
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i ++ )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
int K;
cin >> K;
for (int i = 0; i < K; i ++ )
{
int id;
cin >> id;
add(0, id, 0);
}
spfa();
int Q;
cin >> Q;
for (int i = 0; i < Q; i ++ )
{
int id;
cin >> id;
cout << dist[id] << endl;
}
return 0;
}
题目4 - 设计密码
你现在需要设计一个密码 S,S 需要满足:
- S 的长度是 N;
- S 只包含小写英文字母;
- S 不包含子串 T;
例如:abc
和abcde
是abcde
的子串,abd
不是abcde
的子串。
请问共有多少种不同的密码满足要求?
由于答案会非常大,请输出答案模 109+7 的余数。
数据范围
- 1≤N≤50
- 1≤|T|≤N,|T|是T的长度
- T 只包含小写英文字母
输入格式
N
T
输出格式
一个数,表示总方案数模 109+7。
样例1
输入:
2
a
输出:
625
样例2
输入:
4
cbc
输出:
456924
样例3
输入:
3
xy
输出:
17524
样例4
输入:
50
password
输出:
448890425
算法1
(动态规划+KMP) O(N3)
首先我们用KMP求出 T 的 next 数组。
然后我们回忆利用 next 数组在长文本中匹配模板串 T 的过程:如果下一个字母不匹配,需要一直沿着next指针找:j = next[j]
,直到下一个字符匹配或者next指针指向开头为止。
然后我们会发现,假设我们已经匹配完长文本的前 i 个字母,则剩下部分的匹配过程,只跟next指针的位置有关,因此我们可以用二维数组来表示当前状态的方案数:
f[i][j]
表示匹配完前 i 个字母时,next指针在 j 时的方案总数。
状态转移也很自然:对于每个状态f[i][j]
,我们从'a'-'z'
枚举下一个字母,然后求出对应的next指针,假设是 u,则将f[i][j]
的方案总数累加到f[i+1][u]
。
转移过程中需要注意,因为密码中不能存在 T,所以next指针要避免转移到 T 的最后一个字母。
时间复杂度分析:用KMP求next数组的时间复杂度是 O(N),动态规划的状态有 N2 个,状态转移的计算量是 25N,所以总时间复杂度是 O(25N3)=O(N3)。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#include <string>
using namespace std;
const int N = 55, MOD = 1000000007;
int n, m;
string T;
int f[N][N];
int nxt[N];
void kmp()
{
m = T.size();
T = ' ' + T;
for (int i = 2, j = 0; i <= m; i ++ )
{
while (j && T[j + 1] != T[i]) j = nxt[j];
if (T[j + 1] == T[i]) j ++ ;
nxt[i] = j;
}
}
int main()
{
cin >> n >> T;
kmp();
f[0][0] = 1;
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < m; j ++ )
for (char k = 'a'; k <= 'z'; k ++ )
{
int u = j;
while (u && k != T[u + 1]) u = nxt[u];
if (k == T[u + 1]) u ++;
if (u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % MOD;
}
}
int res = 0;
for (int i = 0; i < m; i ++ ) res = (res + f[n][i]) % MOD;
cout << res << endl;
return 0;
}
算法2
(动态规划)O(N2)
感谢评论区同学提供的一个很优秀的做法!时间复杂度与字符集大小无关,只有 O(N2)。
我们从后往前不断添加新字符。
状态表示:f[i][j] 表示长度为 i,以 T 的末尾 j 个字符开头,且包含 T 的字符串个数。
注意:f[i][j] 并不以最大后缀分类,所以 f[i][j] 和 f[i][k],(j≠k) 可能包含相同字符串。
所以所有包含 T 的字符串都在 f[n][0]中,所以答案就是 26n−f[n][0]。
状态转移:
令 m 表示 T 的长度。
- 如果 i<m,则 f[i][j]=0;
-
如果 i≥m:
当 j=0 时,f[i][j]=26i−m−f[i−1][m−1]+26×f[i−1][0]。- S1 表示以 T 开头,总长度为 m 的字符串集合,|S1|=26m−l;
- S2 表示在 f[m−1][0] 所对应字符串的开头拼上任意字母组成的集合,|S2|=26×f[m−1][0];
- S3 表示以 T 开头,且除了开头,在其它地方也出现过 T 的字符串集合,|S3|=f[m−1][l−1];
- S 表示 f[m][0] 对应的字符串集合;
则 S1−S3 表示以 T 开头,且 T 不出现在其它地方的字符串集合。S2 表示在其它地方出现 T 的字符串集合,所以 S=S1−S3+S2。
当 j≠0 时,- 首先 f[i][j] 可以从 f[i−1][j−1] 转移过来,直接在字符串前面加上 T[−j] 即可;
- 当 T 的前 j 个字符和后 j 个字符匹配时,f[i][j] 同时要加上以 T 开头,但其它地方不包含 T 的字符串数量。
时间复杂度分析:总共 N2 个状态,状态转移的复杂度是 O(1),所以总共时间复杂度是 O(N2)。
C++ 代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 55, MOD = 1000000007;
int n, m;
int f[N][N];
bool ismatch[N];
int pow26[N];
string T;
int pow26f(int k)
{
long long res = 1;
while (k -- ) res = res * 26 % MOD;
return (int)res;
}
bool match(int k)
{
for (int i = 0, j = m - k; i < k; i ++ )
if (T[i] != T[j])
return false;
return true;
}
int main()
{
cin >> n >> T;
m = T.size();
for (int i = 0; i <= n; i ++ ) pow26[i] = pow26f(i);
for (int i = 0; i <= n; i ++ ) ismatch[i] = match(i);
for (int i = 1; i <= n; i ++ )
for (int j = 0; j < m; j ++ )
{
if (i < m) f[i][j] = 0;
else
{
if (!j) f[i][j] = (int)((pow26[i - m] - f[i - 1][m - 1] + 26ll * f[i - 1][0]) % MOD);
else
{
f[i][j] = f[i - 1][j - 1];
if (ismatch[j])
f[i][j] = (pow26[i - m] - f[i - 1][j - 1]) % MOD;
}
}
}
cout << pow26[n] - f[n][0] << endl;
return 0;
}
用f[m][n]表示满足下列三点的字符串的个数:
1 总长度为m
2 以T的末尾n个字符串开头
3 包含T
那么要求的就是26^N - f[N][0]
if (m < l) { f[m][0] = 0; } else { f[m][0] = 26 ^(m - l) + 26 * f[m - 1][0] - f[m - 1][l - 1];// 记S1 = 以T开头总长度为m的字符串的集合, S2 = f[m - 1][0]所对应字符串在开头拼上任意字母组成的集合, S = f[m][0]所对应的字符串的集合, 则|S| = |S1| + |S2| - |S1 ^ S2| } 若n <> 0: if (m < l) { f[m][n] = 0; } else { f[m][n] = f[m - 1][n - 1]; if (T[-n;} match T[0:n]) { f[m][n] += 26 ^ (m - l) - f[m - 1][l - 1]; // 同样记S = f[m][n]对应的集合, S1 = f[m - 1][n- 1]的开头拼上T[-n]对应的集合,S2 = f[m][n]中以T开头的字符串的集合,则|S| = |S1| + |S2| - |S1 ^ S2|; } }
感觉这题有点难啊,不知道上面写的对不对(
同学你的思路很有趣诶,我验证了一下没有问题!
如上文所示,已经采纳进了题解里hh
总觉得最后求转移那里复杂度可以优化
假设我预处理对每个字符k算出最后的u,那么状态转移可否从O(25N)变为O(25),从而整个时间复杂度降到N^2?
可以的,由于给定字符k,以及next指针j以后,我们转移时计算得到的u是确定的,所以可以预处理出所有k和j对应的u,从而把时间复杂度降到O(N2)。
由于这道题目的N最大是50,所以当时就没再进行优化hh