一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
题目链接:420. 强密码检验器
题目描述
如果一个密码满足下述所有条件,则认为这个密码是强密码:
- 由至少 6 个,至多 20 个字符组成。
- 至少包含 一个小写 字母,一个大写 字母,和 一个数字 。
- 同一字符 不能 连续出现三次 (比如 "...aaa..." 是不允许的, 但是 "...aa...a..." 如果满足其他条件也可以算是强密码)。 给你一个字符串 password ,返回 将 password 修改到满足强密码条件需要的最少修改步数。如果 password 已经是强密码,则返回 0 。 在一步修改操作中,你可以:
- 插入一个字符到 password ,
- 从 password 中删除一个字符,或
- 用另一个字符来替换 password 中的某个字符。
提示:
由字母、数字、点 '.' 或者感叹号 '!'
示例 1:
输入:password = "a"
输出:5
示例 2:
输入:password = "aA1"
输出:3
示例 3:
输入:password = "1337C0d3"
输出:0
整理题意: 该题并未涉及主流数据结构和算法,只需要按照题意分类讨论模拟即可。
解题思路
根据题目中的要求,我们可以将给定的字符串按照长度分成三类,进行分类讨论,即:
设字符串长度为n:
- ;
- ;
- ;
- n < 6 的情况: 当 n < 6 时我们对字符串进行删除或者替换操作都是没有意义的,在使用相同次数的操作,直接插入一个字符能够达到同样(或者更好)的效果。因此,我们只考虑插入字符操作,为了保证字符串长度和字符种类均满足题目要求,操作的次数为 6 − n 与 3 - (字符种类) 中的较大值,这里的 6 - n 表示为了满足字符串长度要求的最小操作次数,3 - (字符种类) 中的 (字符种类) 表示仅包含大小写字母和数字这三种字符的字符种类数,所以 3 - (字符种类) 表示还缺少的字符种类数,即满足字符种类的最小操作数。
- 6 <= n <= 20的情况: 当 6 <= n <= 20 时我们对字符串进行删除或者插入操作是没有意义的,在使用相同次数的操作,直接替换一个字符能够达到同样(或者更好)的效果。因此我们只需要考虑替换字符操作,对于连续的 k 个相同的字符,我们可以替换其中 个,使得不存在 3 个连续相同的字符(即每数 3 个字符就替换一次)。同时,我们还需要保证最终字符串包含全部的 3 类字符,因此替换操作的次数为 (所有的 之和) 与 (3−(字符种类)) 中的较大值。
- 20 < n的情况: 当 20 < n 时我们对字符串进行插入操作是没有意义的,可以这么想,如果插入了一个字符,但是最后为了满足题目字符串长度要求,还得删除一个字符,那么在使用相同次数的操作,不使用插入字符操作能够达到同样(或者更好)的效果。那么这里还剩下替换操作和删除操作,首先我们可以考虑将字符串删除到第2种情况(6 <= n <= 20),然后再进行替换操作,那么问题来了,怎么删除才能使得最后替换时的操作次数最小呢,我们删除连续长度大于等于3的字符串时能够使得最后替换字符操作的次数减少,但是面对多个连续长度大于等于3的字符串时我们该如何选择才能使得答案最优呢,这里举例说明,比如
aaa,bbbb,ccccc这三个字符串,对于这三个字符,在最后替换的时候都需要1次替换来使得它们满足题目要求,但是这三个字符串对于删除的次数却不相同,aaa需要删除1个字符也就是1次删除操作,bbbb需要2次删除操作,ccccc需要3次删除操作,我们想的是利用最少的删除次数,最大的减少最后替换的次数,因此在删除字符时,我们优先从所有 的连续相同字符组中删除 1 个字符(这样可以使得替换字符的操作次数同时减少 1),其次从所有 的连续相同字符组中删除 2 个字符,最后每删除 3 个字符,可以使得替换的操作次数减少 1。最终删除需要的次数为 n - 20,替换需要的次数为 (所有的 之和) ,但可以通过删除字符操作省去若干次替换字符操作,最后得到的真正需要的替换操作次数还需要与 (3−(字符种类)) 取较大值,保证最终字符串包含全部的 3 类字符。
题目总结
该题字符串长度不超过20时较容易处理,超过20时正确处理替换操作和删除操作的方法较难想到。
代码部分
class Solution {
public:
int strongPasswordChecker(string password) {
int n = password.length();
//判断password中是否含有小写、大写字母和数字;
bool lower, upper, digit;
lower = upper = digit = false;
for(int i = 0; i < n; i++){
if(islower(password[i])) lower = true;
if(isupper(password[i])) upper = true;
if(isdigit(password[i])) digit = true;
}
//含有大小写字母和数字的种类数
int sum = lower + upper + digit;
//如果长度n < 6,插入字符操作为最优,插入个数取最大值
if(n < 6) return max(6 - n, 3 - sum);
//如果长度n <= 20,替换字符操作为最优,替换个数为连续字符/3的总和
else if(n <= 20){
int k = 0, num = 1;
for(int i = 1; i < n; i++){
if(password[i] == password[i - 1]) num++;
else{
k += num / 3;
num = 1;
}
}
//考虑字符串尾部连续字符情况
k += num / 3;
//保证字符种类满足要求,取max
return max(k, 3 - sum);
}
//难点:如果n > 20
else{// n > 20
//统计替换次数replace和移除次数remove
int replace = 0, remove = n - 20;
//记录连续字符长度num,记录需要移除两个字符(num % 3 == 1)的个数
int num = 1, rm2 = 0;
for(int i = 1; i < n; i++){
if(password[i] == password[i - 1]) num++;
else{
//优先删除一个字符的情况,使得变为num % 3 == 2,每移除1个字符减少1次替换
if(remove > 0 && num % 3 == 0){//是可以保证num != 0
remove--;
replace--;
}
//记录需要移除两个字符的个数,使得变为num % 3 == 2,注意这里需要num > 3,才能移除2个字符
else if(num > 3 && num % 3 == 1) rm2++;
//剩下的都是num % 3 == 2的情况,无需显式记录,每移除3个字符减少1次替换
replace += num / 3;
num = 1;
}
}
//考虑字符串尾部连续字符情况
if(remove > 0 && num % 3 == 0){
remove--;
replace--;
}
else if(num > 3 && num % 3 == 1) rm2++;
replace += num / 3;
//处理num % 3 == 1的情况也就是rm2记录的个数,同时要考虑replace和remove的次数
int use2 = min({rm2, replace, remove / 2});
//每移除2个字符减少1次替换
replace -= use2;
remove -= use2 * 2;
//处理num % 3 == 2的情况,考虑每移除3个字符,减少1次替换
int use3 = min({replace, remove / 3});
//因为replace记录了每3个连续字符就需要替换1次,所以replace即为num % 3 == 2的情况个数
replace -= use3;
remove -= use3 * 3;
//n - 20次remove是必须的,但是在这n - 20次remove中可以减少replace的次数
//最后replace的次数还需要与缺少的字符种数取最大值,保证字符种类满足要求
return n - 20 + max({replace, 3 - sum});
}
}
};
结束语
默默耕耘,是为了在明天硕果累累;沉淀积累,是为了在未来厚积薄发。成功不是一蹴而就,唯有潜心笃志、积蓄力量,才能迎来开花之时。