一、题目解析
我们先来总结一下题意:
(1) 26个字母(a-z),最多6个
(2) 第1位可以是a-z中任何一个。
(3) 第2位只能是在第1位字符的后面字符。第3位只能是在第2位后面的字符,后面也是一样的。
(4) 这样编号后,输入一个字符串,问:它的编号是多少?如果没找到,就输出0.
二、dfs解法
看着数量不多啊,实不行就(dfs)得到所有组合,一个个加进去,然后查数应该也能(AC)~
无耻的发一轮(dfs)的代码:
#include <bits/stdc++.h>
using namespace std;
int cnt;
unordered_map<string, int> _map;
string str;
/**
* 功能:预处理填充所有的1-6位字符串+数字映射表
* @param len 要填充几位的字符串:1,2,3,4,5,6
* @param pos 在指定长度的字符串前提下,当前正在填充第几位
*/
void dfs(int len, int pos) {
//递归出口,走冒了,就停止
if (pos > len) {
_map[str] = ++cnt;
return;
}
//默认第1位是从a开始的
char start = 'a';
//如果是2,3,4,5,6位,那么开始的字符应该是当前填充字符串的前面那位+1
//为什么是k-2呢?这是因为k=1时,start='a';k=2时,需要找now[0]的值然后+1
if (pos > 1)start = str[pos - 2] + 1;
//从比最后一位大的字符开始,一直到z,都是可以走的路径,分别派出侦查兵进行探索
for (char i = start; i <= 'z'; i++) {
//填充本位置
str[pos - 1] = i;//字符串的下标是从0开始,所以,第k个字符,下标索引是k-1
//继续讨论下一个位置
dfs(len, pos + 1);
}
}
int main() {
//预处理:不同长度的单词分别处理,len从1到6(最长6位)
for (int len = 1; len <= 6; len++) {
//对字符串进行清理和长度设定,这是因为后面需要向指定的位置写入字符,如果不说好长度的话就没法写入
str.clear();
str.resize(len);
//开始深搜
dfs(len, 1);
}
//输入
string ask;
cin >> ask;
//查询输出
cout << _map[ask] << endl;
return 0;
}
三、组合数求法
非常经典的组合数公式题!这一题,放在第20章来讲,肯定是想让我们练习组合数的功能,上来就暴搜,太不了解出题人心理啦,差评!
那这题和组合数有个毛线关系啊?
拿cgx
举例,设(ans)是比cgx
小的单词个数,初值是(0)。
分情况讨论,然后加法原理,把所有可能性加在一起。
-
首先,
cgx
肯定比只有一个字母的单词大,(huge C_{26}^1) ,(ans+C_{26}^1,ans+26,ans=26)。 -
其次,
cgx
肯定比只有两个字符的单词大,
只有两个字母的单词数该怎么算呢?就是在(26)个字母中选(2)个。(huge C_{26}^2)
(ans+C_{26}^2=ans+frac{26 imes 25}{2 imes 1}=ans+325=26+325=351)
为什么是这样算呢?比如我们想一下,如果第一个字母是a
,那么后面只能是b,c,d,...,z
,共25个,如果第一个字母是b
,那么是24个,如果第一个字母是y
,那么是1个,加法原理就是(1+2+3+4+...+25=325)个!
殊途同归啊,一样的(325),这是为什么呢?仔细思考发现,(C_{26}^2)可以这样来理解,就是在(26)个字母中找出(2)个,然后肯定有一个大一个小,然后把小的在前,大的在后放上去,就是一种办法,可不对咋的。每一步一定要仔细思考啊,确定自己真正明白了才行啊~
同含义代码:
// 1位的有C(26,1)个
// 2位的有C(26,2)个
// 为什么i从1到len-1呢?因为这里是找所有位数比len小的,等于len的单独讨论。
for (int i = 1; i < len; i++)ans += C(26, i);
- 讨论三个字母的情况
-
第一位:它比以字母
a-b
为第一位的大(cgx
第一位为c
,所以要比c
小)以
a
为第一位,且有三位的单词个数:(C_{25}^2=frac{25*24}{2*1}=300)从比
a
大的剩下(25)个字母中选2个字母:(ans+300,ans=351+300=651)。以
b
为第一位,且有三位的个数:(C_{24}^2=frac{24*23}{2*1}=276)。从比
b
大的剩下(24)个字母中选(2)个字母:(ans+276,ans=651+276=927)。 -
第二位: 即第一位已经确定为(c)。它比以字母
d-f
为第二位的单词大(第一位为c
,所以要比c
大,第二位为g
,所以要比g
小)第二位为
d
的个数:(C_{22}^1=frac{22}{1}=22)。从比
d
大的剩下(22)个字母中选(1)个字母: (ans+22,ans=927+22=949)。第二位为
e
的个数:(C_{21}^1,ans+21,ans=949+21=970)。第二位为
f
的个数:(C_{20}^1,ans+20,ans=970+20=990)。 -
第三位:即第一、二位已经确定:它比以字母
h-w
为第三位的大,共有(16)个。 (因为cgx
的第二位是g
,所以需要比g
大,就是从h
开始,同时,第三位是x
,还得比x
小,就是最大是w
)。
(w-h+1=16),共(16)个。(ans+167,ans=990+16=1006)因为比
cgx
小的单词共有(1006)个,所以cgx
是第(1007)个。
根据上面的人脑模拟过程,发现要遍历每一位,即就外层循环是:
for (int pos = 0; pos < len; pos++) {
...
}
为什么(pos=0)到(pos<len)结束呢?因为字符串的数组索引下标是从(0)开始到(str.size())结束的。
那循环里面需要做点什么呢?
(1)找到合适的起始字符
如果是pos=0,则起始是'a'。
如果pos>0,则起始是前一个位置的字符+1。
(2)算出合适的结束字符
结束字符,是输入字符串当前位置字符-1:也就是小于str[pos]。
(3) 分别讨论此位置是不同字符情况下的组合数
for (char j = start; j < str[pos]; j++){
}
剩余的字符个数:('z'-j)。
需要挑选中的字符个数:(len - pos - 1)
组合数:(huge C_{'z'-j}^{len-pos-1})
先判断该单词是否存在,再一位一位地去考虑共有多少比给出单词小的单词,如果用的是字符串,则还需要考虑一下边界问题。
#include <bits/stdc++.h>
using namespace std;
string str;
int ans;
//求组合数的优化后办法
long long C(int n, int m) {
long long sum = 1;
for (int i = 1; i <= m; i++)
sum = sum * (n - m + i) / i;
return sum;
}
/**
* 测试用例:cgx
* 答案:1007
* @return
*/
int main() {
//输入字符串
cin >> str;
//字符串长度
int len = str.size();
//判断是否合法
for (int i = 0; i < len; i++)
if (str[i] <= str[i - 1]) {//非升序就是不正确的编码
cout << 0;
exit(0);
}
//1、计算出位数比它小的单词数
//比如是cgx,那么就计算出
// 1位的有C(26,1)个
// 2位的有C(26,2)个
for (int i = 1; i < len; i++)ans += C(26, i);
//2、到这就是位数和它自己一样的了
// 枚举每一位,从低到高,一个个来
for (int pos = 0; pos < len; pos++) {
//每一位都要找到比当前输入字符串小的字符串个数,叠加在一起就是结果
char start = 'a';//初值为`a`,首位的话,从`a`开始,其它的从上一位的后面一个开始
if (pos > 0)start = str[pos - 1] + 1; //str的数组下标是从0开始的
for (char j = start; j < str[pos]; j++)//注意范围,找比它小的
ans += C('z' - j, len - pos - 1); //'z' - j 剩下可用的字符数量, len-pos-1需要选择的字符数量
}
//别忘了最后把自己加上
cout << ++ans;
return 0;
}