【ACWing】1052. 设计密码

题目地址:

https://www.acwing.com/problem/content/1054/

你现在需要设计一个密码 S S S S S S需要满足: S S S的长度是 N N N S S S只包含小写英文字母; S S S不包含子串 T T T;例如: a b c abc abc a b c d e abcde abcde a b c d e abcde abcde的子串, a b d abd abd不是 a b c d e abcde abcde的子串。请问共有多少种不同的密码满足要求?由于答案会非常大,请输出答案模 1 0 9 + 7 10^9+7 109+7的余数。

输入格式:
第一行输入整数 N N N,表示密码的长度。第二行输入字符串 T T T T T T中只包含小写字母。

输出格式:
输出一个正整数,表示总方案数模 1 0 9 + 7 10^9+7 109+7后的结果。

数据范围:
1 ≤ N ≤ 50 1≤N≤50 1N50
1 ≤ ∣ T ∣ ≤ N 1≤|T|≤N 1TN ∣ T ∣ |T| T T T T的长度。

字符串匹配的过程可以用状态机来模拟。实际上KMP算法就是在模拟一种状态机,该状态机是依据模式串生成,并且可以接受以模式串为子串的任意字符串。即,我们先由模式串生成KMP里的next数组,接着用这个数组构造出一个有限状态自动机,使得这个DFA(Deterministic Finite Automaton,确定有限状态自动机的英文缩写)只接受以该模式串为子串的字符串。我们以字符串"aba"为模式串举例,具体来说如下:
1、先求出s = "aba"的next数组。参考https://blog.csdn.net/qq_46105170/article/details/113805346。这里的next数组可以是未优化版的也可以是优化版的,因为本质上来说,它们构造出的DFA识别的语言是一样的。
2、构造一个DFA,它有三个状态,分别是 0 , 1 , 2 , 3 0,1,2,3 0,1,2,3 0 0 0是初始状态, 3 3 3是接受状态(该DFA只有一个接受状态)。先构造边 δ ( 0 , a ) = 1 , δ ( 1 , b ) = 2 , δ ( 2 , a ) = 3 \delta(0,a)=1,\delta(1,b)=2,\delta(2,a)=3 δ(0,a)=1,δ(1,b)=2,δ(2,a)=3,这三个转移边是显然的,对应的情况是恰好存在子串"aba"。接下来考虑失配边,这个字符串的next数组是 n e = [ − 1 , 0 , 0 ] n_e=[-1,0,0] ne=[1,0,0],这个数组规定了如果在 s [ i ] s[i] s[i]存在失配,应该如何跳转。跳转规则如下:比如在 s [ 1 ] s[1] s[1] s [ 2 ] s[2] s[2]处失配的时候(对应的是当前状态在 1 1 1 2 2 2的时候,比如当前位于状态 1 1 1,然后读入了'a'字符,这样就发生了失配),比如当前读到的字符是'c',那么就回到状态 0 0 0,然后继续在状态 0 0 0处匹配'c'(也就是看一下 s [ 0 ] s[0] s[0]是否等于'c'),仍然不匹配,那么就跳到 n e [ 0 ] = − 1 n_e[0]=-1 ne[0]=1,这个 − 1 -1 1并不是一个真实状态,它是一个虚拟的状态,假想 s [ − 1 ] s[-1] s[1]是一个通配符,可以匹配任意字符,那么此时就匹配上了,沿着这条虚拟的边走到状态 0 0 0(所以我们可以特判一下状态 − 1 -1 1就行了,不需要真在程序里写这个状态);再比如,在状态 1 1 1的时候读到了字符'b',那么此时是匹配的,直接沿着匹配边走到下一个状态,也就是状态 2 2 2
3、总结一下该DFA的跳转规则。当当前位于状态 i i i,并且读入了字符 α \alpha α的时候,进行如下循环:只要当前处于的状态 j j j不是 − 1 -1 1,并且 s [ j ] s[j] s[j] α \alpha α不匹配,那么就跳转到 n e [ j ] n_e[j] ne[j]去,直到 j = − 1 j=-1 j=1或者 s [ j ] = α s[j]=\alpha s[j]=α为止,此时沿着匹配边走一步到状态 j + 1 j+1 j+1去。总结来说就是 δ ( i , α ) = j + 1 \delta(i,\alpha)=j+1 δ(i,α)=j+1

由于上述DFA接受某个字符串,当且仅当其以构造该DFA的模式串为子串。这样一来,要求不含 S S S为子串的字符串数量,相当于就是在问,从DFA的状态 0 0 0出发,跳转 N N N次,并且中途和终点没有跳到接受状态 l S l_S lS的路径个数。这可以用动态规划来做。设 f [ k ] [ p ] f[k][p] f[k][p]是跳 k k k步跳到状态 p p p的路径条数,那么可以按照跳到状态 p p p之前在哪儿来分类,则有: f [ k ] [ p ] = ∑ q → p f [ k − 1 ] [ q ] f[k][p]=\sum_{q\to p} f[k-1][q] f[k][p]=qpf[k1][q]初始条件 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1(因为初始状态就是状态 0 0 0)。最终答案就是: ∑ p = 0 l S − 1 f [ N ] [ p ] \sum_{p=0}^{l_S-1} f[N][p] p=0lS1f[N][p]即跳 N N N步没有跳到状态 l S l_S lS的路径条数。

由于不方便知道某个状态之前是哪个状态,我们可以用当前状态来更新未来状态,即可以用 f [ k − 1 ] [ q ] f[k-1][q] f[k1][q]来累加到 f [ k ] [ p ] f[k][p] f[k][p]上去。代码如下:

#include <iostream>
#include <cstring>
using namespace std;

const int N = 55, mod = 1e9 + 7;
int n, m;
char s[N];
int f[N][N];
int ne[N];

// 求next数组
void build_ne() {
  ne[0] = -1;
  for (int i = 0, j = -1; i < m - 1;)
    if (j < 0 || s[j] == s[i]) {
      i++;
      j++;
      ne[i] = s[i] != s[j] ? j : ne[j];
    } else j = ne[j];
}

int main() {
  scanf("%d%s", &n, s);
  m = strlen(s);
  build_ne();

  f[0][0] = 1;
  // 枚举步数
  for (int i = 1; i <= n; i++)
    // 枚举当前在哪个状态
    for (int j = 0; j < m; j++)
      // 枚举当前在状态j的时候获得的输入字符
      for (char ch = 'a'; ch <= 'z'; ch++) {
        // 开始计算从状态j开始,读入ch后会跳到哪个状态
        int u = j;
        while (u != -1 && ch != s[u]) u = ne[u];
        u++;
        // 状态s.size()是接受态,走到其的路径条数不用计算,否则累加一下路径条数
        if (u < m) f[i][u] = (f[i][u] + f[i - 1][j]) % mod;
      }

  int res = 0;
  for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
  printf("%d\n", res);
}

时间复杂度 O ( N l S 2 ) O(Nl_S^2) O(NlS2),空间 O ( N l S ) O(Nl_S) O(NlS)

下面给出字符串下标从 1 1 1开始的版本。此版本中,设字符串长度 m m m,那么状态是 0 , 1 , . . . , m 0,1,...,m 0,1,...,m这些,其中 m m m是匹配状态,本题中不考虑。每个状态 u u u在读入字符串 c c c的时候,当 c ≠ s [ u + 1 ] c\ne s[u+1] c=s[u+1]的时候发生跳转,一直跳转到 n e k [ u ] ne^k[u] nek[u]直到 u = 0 ∨ c = s [ u + 1 ] u=0\lor c= s[u+1] u=0c=s[u+1]成立,如果 c = s [ u + 1 ] c=s[u+1] c=s[u+1] u u u向后跳一格。代码如下:

#include <iostream>
#include <cstring>
using namespace std;

const int N = 55, mod = 1e9 + 7;
int n, m;
char s[N];
int f[N][N], ne[N];

void build_ne() {
  for (int i = 2, j = 0; i <= m; i++) {
    while (j && s[i] != s[j + 1]) j = ne[j];
    if (s[i] == s[j + 1]) j++;
    ne[i] = i < m && s[i + 1] != s[j + 1] ? j : ne[j];
  }
}

int main() {
  cin >> n >> s + 1;
  m = strlen(s + 1);
  build_ne();

  f[0][0] = 1;
  for (int i = 0; i < n; i++)
    for (int j = 0; j < m; j++)
      for (char ch = 'a'; ch <= 'z'; ch++) {
        int u = j;
        while (u && s[u + 1] != ch) u = ne[u];
        if (s[u + 1] == ch) u++;
        if (u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
      }

  int res = 0;
  for (int j = 0; j < m; j++) res = (res + f[n][j]) % mod;
  printf("%d\n", res);
}

时空复杂度一样。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
堆排序是一种基于堆数据结构的排序算法。它通过将待排序的序列构建成一个大顶堆(或小顶堆),然后依次将堆顶元素与最后一个元素交换,并重新调整堆,直到整个序列有序。 具体的堆排序算法如下: 1. 构建初始堆:将待排序序列构建成一个大顶堆。从最后一个非叶子节点开始,依次向前调整每个节点,使其满足大顶堆的性质。这一过程称为“下沉”操作。 2. 将堆顶元素与最后一个元素交换:将堆顶元素与待排序序列的最后一个元素进行交换,此时最后一个元素即为当前序列的最大值。 3. 重新调整堆:将交换后的堆顶元素进行“下沉”操作,使其满足大顶堆的性质。 4. 重复步骤2和3,直到整个序列有序。 下面是C++实现堆排序的代码: ```cpp #include <iostream> #include <vector> using namespace std; // 下沉操作 void heapify(vector<int>& nums, int n, int i) { int largest = i; // 初始化最大值为当前节点 int left = 2 * i + 1; // 左子节点 int right = 2 * i + 2; // 右子节点 // 如果左子节点大于根节点,则更新最大值 if (left < n && nums[left] > nums[largest]) { largest = left; } // 如果右子节点大于最大值,则更新最大值 if (right < n && nums[right] > nums[largest]) { largest = right; } // 如果最大值不是当前节点,则交换节点,并继续调整堆 if (largest != i) { swap(nums[i], nums[largest]); heapify(nums, n, largest); } } // 堆排序 void heapSort(vector<int>& nums) { int n = nums.size(); // 构建初始堆,从最后一个非叶子节点开始向前调整 for (int i = n / 2 - 1; i >= 0; i--) { heapify(nums, n, i); } // 依次将堆顶元素与最后一个元素交换,并重新调整堆 for (int i = n - 1; i > 0; i--) { swap(nums[0], nums[i]); heapify(nums, i, 0); } } int main() { vector<int> nums = {4, 2, 6, 8, 5, 7}; heapSort(nums); cout << "Sorted array: "; for (int num : nums) { cout << num << " "; } return 0; } ``` 以上是堆排序的C++实现代码,通过构建初始堆和重复交换堆顶元素的过程,最终实现了对序列的排序。时间复杂度为O(nlogn),其中n是序列的长度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值