原题链接:
Constanze is the smartest girl in her village but she has bad eyesight.
One day, she was able to invent an incredible machine! When you pronounce letters, the machine will inscribe them onto a piece of paper. For example, if you pronounce ‘c’, ‘o’, ‘d’, and ‘e’ in that order, then the machine will inscribe “code” onto the paper. Thanks to this machine, she can finally write messages without using her glasses.
However, her dumb friend Akko decided to play a prank on her. Akko tinkered with the machine so that if you pronounce ‘w’, it will inscribe “uu” instead of “w”, and if you pronounce ‘m’, it will inscribe “nn” instead of “m”! Since Constanze had bad eyesight, she was not able to realize what Akko did.
The rest of the letters behave the same as before: if you pronounce any letter besides ‘w’ and ‘m’, the machine will just inscribe it onto a piece of paper.
The next day, I received a letter in my mailbox. I can’t understand it so I think it’s either just some gibberish from Akko, or Constanze made it using her machine. But since I know what Akko did, I can just list down all possible strings that Constanze’s machine would have turned into the message I got and see if anything makes sense.
But I need to know how much paper I will need, and that’s why I’m asking you for help. Tell me the number of strings that Constanze’s machine would’ve turned into the message I got.
But since this number can be quite large, tell me instead its remainder when divided by 10^9+7.
If there are no strings that Constanze’s machine would’ve turned into the message I got, then print 0.
Input
Input consists of a single line containing a string s (1≤|s|≤10^5) — the received message. s contains only lowercase Latin letters.
Output
Print a single integer — the number of strings that Constanze’s machine would’ve turned into the message s, modulo 10^9+7.
Examples
input
ouuokarinn
output
4
input
banana
output
1
input
nnn
output
3
input
amanda
output
0
可以将所有可能的情况数看作为求最优值(得到所有可能的情况数即为最优数),当前字符串的所有可能的情况数必定由子字符串的所有可能的情况数组成所以具有最优子结构性,显然子字符串可能反复出现,具有子问题重叠性,因此可以用 dp 来解决该问题。
对于如“…nn”,由于最后的“nn”可以拆开来看成“n”与“n”,前一个 n 与前面的字符串够成的子字符串的所有情况数为拆开时的所有情况数。同时最后的“nn”也可以合并看成一个“m”,此时的所有情况数取决于前面的字符串够成的子字符串的所有情况数。所以综合来看,“…nn”的所有情况数为拆开与合并时两个所有情况数的累加。由此关系可以得出递归公式,dp[i]=dp[i-1]+dp[i-2](dp[i]表示当前的最优值)(这也是为什么符合斐波那契数列的原因)。
显然,当遇到不是连续的“nn”或“uu”时 dp[i]=dp[i-1],因为其他字母并不影响情况数,直接继承上一个子字符串的所有情况数即可。
记 dp[i]为当前字符串的所有情况数。根据上述分析可以得到如下递归关系:
d p [ i ] = { d p [ i − 1 ] + d p [ i − 2 ] , s [ i ] = s [ i − 1 ] a n d s [ i ] , s [ i − 1 ] ∈ { n , u } d p [ i − q ] , s [ i ] ≠ s [ i − 1 ] dp[i]= \begin{cases} dp[i−1] + dp[i−2] \quad ,s[i] = s[i−1] \quad and \quad s[i], s[i−1] \in \{n, u\} \\ dp[i-q] \quad ,s[i] \neq s[i-1] \end{cases} dp[i]={dp[i−1]+dp[i−2],s[i]=s[i−1]ands[i],s[i−1]∈{n,u}dp[i−q],s[i]=s[i−1]
下面采用自底向上的方法实现dp。
#include
#include
using namespace std;
const long long int MOD=1e9+7;
long long int dp[100005];
string s;
long long int Constanze()
{
cin >> s;
int n = s.length();
if (s[0]=='m' || s[0]=='w'){
return 0;
}
if (n>=2){
if (s[1]=='m' || s[1]=='w'){
return 0;
}
}
//
dp[0]=1;
dp[1]=1;
//
for (int i=2;i<=n;i++){
if (s[i]=='m' || s[i]=='w'){
return 0;
}
if ((s[i-1]=='n' && s[i-2]=='n') || (s[i-1]=='u' && s[i-2]=='u')){
dp[i] = dp[i-1] + dp[i-2];
}
else{
dp[i]=dp[i-1];
}
//
dp[i]%=MOD;
}
return dp[n];
}
int main()
{
int res = Constanze();
cout << res << endl;
return 0;
}
时间复杂度为 Θ ( n ) \Theta(n) Θ(n)
根据题意,造成有多种可能的结果来源于连续的“nn”和“uu”字符串的存在,如“nn”,既可以合并为一个字母“m”,也可以拆开为两个字母“nn”,共有两种情况。通过简单的概率统计的知识可以得到,所有情况等于连续的“nn”或“uu”的区间分别可能产生的情况数的乘积。
如“snnuxuuxzuuu”的所有情况数为 223=12 种。对于,连续的“nn”或“uu”的区间可能产生的情况数的求法,观察规律可发现,“u”=1,“uu”=2,“uuu”=3,“uuuu”=5,4 个连续的“u”的情况数为 2 个和 3 个连续的“u”的情况数的累加和,即符合斐波那契数列的性质,通过该方法就能得出一段连续的“nn”或“uu”的区间可能产生的情况数。
当然,通过分析可以得出,只要有“m”或“w”字符串的出现说明机器是好的,并不属于改造后的机器范畴,不存在有没有多种情况的问题,可以直接记为 0。
#include
#include
#define LL long long int
using namespace std;
const long long int MOD=1e9+7;
string s;
//输入连续的n或u的个数,返回可能产生的情况数
LL PNumbers(int l)
{
if (l==2){
return 2;
}
if (l==3){
return 3;
}
LL a=2,b=3,res=0;
for (int i=4;i<=l;i++){
res=a+b;
res%=MOD;
a=b;
b=res;
}
return res;
}
LL Constanze(){
cin >> s;
//用于存放全区间连续的n或u的个数
int record[100005];
int j=0;
//用于记录单个区间连续的n和u的个数,初始化为1
int u=1,n=1;
//特判
if (s[0]=='m' || s[0]=='w'){
return 0;
}
for (int i=1;i<s.length();i++){
if (s[i]=='m' || s[i]=='w'){
return 0;
}
if (s[i]=='u' && s[i-1]=='u'){
u++;
}
//当不是连续的u时重置 ,u>1时为了避免无意义的重置操作
else if(u>1){
record[j] = PNumbers(u);
j++;
u = 1;
}
if (s[i]=='n' && s[i-1]=='n'){
n++;
}
else if(n>1){
record[j] = PNumbers(n);
j++;
n = 1;
}
}
//对于末尾情况,特殊处理
if (n>1){
record[j] = PNumbers(n);
j++;
}
if (u>1){
record[j] = PNumbers(u);
j++;
}
LL final=1;
for (int i=0;i<j;i++){
final*=record[i];
final%=MOD;
}
return final;
}
int main()
{
LL rs = Constanze();
cout << rs << endl;
return 0;
}
时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)
根据实现的代码,朴素统计法需要遍历一次整个字符串 s,而每次统计连续的“nn”或“uu”后需要调用 PNumbers 函数,该函数每次都是从头开始计算斐波那契数列,时间复杂度为 O ( n ) O(n) O(n),因此整个算法最坏情况下的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)。
对于动态规划,不管什么情况下只需要遍历一次整个字符串即可,所以时间复杂度为 Θ ( n ) \Theta(n) Θ(n)。
因此明显动态规划法解决该问题比朴素统计法更优秀。朴素统计法改进可以像动归一样牺牲空间来改进,避免重复算子问题(因为连续的“uu”或“nn”的个数会有重叠的地方,用斐波那契数列方法计算后可以用数组记录相应连续长度位置的结果)
动态规划法实现的代码还有两点能改进的地方,首先可以先遍历一遍字符串s 来查看是否有“m”后“w”,只要出现就返回 0,这样对于在字符串最后结尾包含“m”或“w”的例子会高效多,因为前面可以省去计算的时间。
另外,由于实现时 s[i]对应的记录是 dp[i+1],不太方便理解,可以如下改进,使得 s 与 dp对应的下标一致:
dp[0]=1;
if (s.substr(0,2)=="nn" || s.substr(0,2)=="uu"){
dp[1]=2;
}
else{
dp[1]=1;
}
for(int i=2;i<n;i++){
if (s[i]==s[i-1] && (s[i]=='n'||s[i]=='u')){
dp[i] = (dp[i-1]+dp[i-2])%MOD;
}
else{
dp[i]=dp[i-1];
}
}
return dp[n-1];