第三题:
给你一个长度为 n
下标从 0 开始的整数数组 nums
。
我们想将下标进行分组,使得 [0, n - 1]
内所有下标 i
都 恰好 被分到其中一组。
如果以下条件成立,我们说这个分组方案是合法的:
- 对于每个组
g
,同一组内所有下标在nums
中对应的数值都相等。 - 对于任意两个组
g1
和g2
,两个组中 下标数量 的 差值不超过1
。
请你返回一个整数,表示得到一个合法分组方案的 最少 组数。
示例 1:
输入:nums = [3,2,3,2,3] 输出:2 解释:一个得到 2 个分组的方案如下,中括号内的数字都是下标: 组 1 -> [0,2,4] 组 2 -> [1,3] 所有下标都只属于一个组。 组 1 中,nums[0] == nums[2] == nums[4] ,所有下标对应的数值都相等。 组 2 中,nums[1] == nums[3] ,所有下标对应的数值都相等。 组 1 中下标数目为 3 ,组 2 中下标数目为 2 。 两者之差不超过 1 。 无法得到一个小于 2 组的答案,因为如果只有 1 组,组内所有下标对应的数值都要相等。 所以答案为 2 。
示例 2:
输入:nums = [10,10,10,3,1,1] 输出:4 解释:一个得到 2 个分组的方案如下,中括号内的数字都是下标: 组 1 -> [0] 组 2 -> [1,2] 组 3 -> [3] 组 4 -> [4,5] 分组方案满足题目要求的两个条件。 无法得到一个小于 4 组的答案。 所以答案为 4 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
这道题的关键在于将nums数组转换成次数数组之后,怎么解决不同次数的分割问题。
首先利用HashMap遍历nums数组,我们可以得到一个各个数字以及出现次数的键值对cnt.题目中要求每个划分的数量差值最大为1,所以关键是最小值的那个键值对。我们记录最小value键值对为min,通过遍历cnt.value()我们可以得到这个最小值。
但是我们得到这个最小值怎么用呢?如果用min,min-1或者min+1去划分剩下的value,那么就有可能会出现这样一种情况:cnt = {3,4,5} 如果用3去划分,就出现{3,3,1,3,2},显然3-1 = 2,不成立。同理,4也不行。min-1在本案例中可以。但是在更加复杂的用例中可能出现需要min-2甚至min-3的情况,这就没法解决了。那么有什么好的解法呢?且看大佬解法:
lass Solution {
public int minGroupsForValidAssignment(int[] nums) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int x : nums) {
cnt.merge(x, 1, Integer::sum);
}
int k = nums.length;
for (var c : cnt.values()) {
k = Math.min(k, c);
}
for (; ; k--) {
int ans = 0;
for (int c : cnt.values()) {
if (c / k < c % k) {
ans = 0;
break;
}
ans += (c + k) / (k + 1);
}
if (ans > 0) {
return ans;
}
}
}
}
是否有点看不懂呢? 这里我来详细解说一下。首先一样的求出这个数量散列,再遍历value值得到最小的值k。
有一个很显然的定理,当一个key的出现次数c%k <= floor(c/k)时,我们可以将c%k多出来的数量平均分配到总组数floor(c/k)当中去。如:32 % 10 = 2, 32/10=3,那么我们就可以把2平分到两个组中,这样得到的结果就是11,11,10。最大值11也就比k大。但是如果c%k>floor(c/k)时我们就不能这样干了。只能寻求k<10以后的划分了。如{3,4,5}需要寻求2的划分一样。
为了防止k-1甚至k-2都无法完成划分,我们安排k--循环来求解。每次遍历都意味着新的划分,因此我们每次循环维持一个记录数ans=0。这时我们就可以遍历所有value值了。当c/k < c%k时,我们就需要跳出循环了。同时因为无法划分完成,我们需要将ans值置零。需要等待k--。若c/k >= c%k,那么我们可以进行划分,但是能够划分成多少组呢?答案是ceil(c/k)。以下是简单表示ceil(c/k)的方式:
ceil(c / k) = (c + k) / (k + 1)
当我们循环完成没有跳出,就可以返回划分值了。题解完毕。
第四题:
给你一个字符串 s
和一个整数 k
,请你将 s
分成 k
个 子字符串 ,使得每个 子字符串 变成 半回文串 需要修改的字符数目最少。
请你返回一个整数,表示需要修改的 最少 字符数目。
注意:
- 如果一个字符串从左往右和从右往左读是一样的,那么它是一个 回文串 。
- 如果长度为
len
的字符串存在一个满足1 <= d < len
的正整数d
,len % d == 0
成立且所有对d
做除法余数相同的下标对应的字符连起来得到的字符串都是 回文串 ,那么我们说这个字符串是 半回文串 。比方说"aa"
,"aba"
,"adbgad"
和"abab"
都是 半回文串 ,而"a"
,"ab"
和"abca"
不是。 - 子字符串 指的是一个字符串中一段连续的字符序列。
示例 1:
输入:s = "abcac", k = 2 输出:1 解释:我们可以将 s 分成子字符串 "ab" 和 "cac" 。子字符串 "cac" 已经是半回文串。如果我们将 "ab" 变成 "aa" ,它也会变成一个 d = 1 的半回文串。 该方案是将 s 分成 2 个子字符串的前提下,得到 2 个半回文子字符串需要的最少修改次数。所以答案为 1 。
示例 2:
输入:s = "abcdef", k = 2 输出:2 解释:我们可以将 s 分成子字符串 "abc" 和 "def" 。子字符串 "abc" 和 "def" 都需要修改一个字符得到半回文串,所以我们总共需要 2 次字符修改使所有子字符串变成半回文串。 该方案是将 s 分成 2 个子字符串的前提下,得到 2 个半回文子字符串需要的最少修改次数。所以答案为 2 。
示例 3:
输入:s = "aabbaa", k = 3 输出:0 解释:我们可以将 s 分成子字符串 "aa" ,"bb" 和 "aa" 。 字符串 "aa" 和 "bb" 都已经是半回文串了。所以答案为 0 。
提示:
2 <= s.length <= 200
1 <= k <= s.length / 2
s
只包含小写英文字母。
class Solution {
final int mx = 201;
List<Integer>[] divisors = new List[201];
public int minimumChanges(String s, int k) {
Arrays.setAll(divisors,t->new LinkedList<>());
for(int i=1;i<mx;i++){
for(int j=i*2;j<mx;j+=i){
divisors[j].add(i);
}
}
int n = s.length();
int[][] modify = new int[n-1][n];
for(int l = 0;l <n-1;l++){
for(int r = l+1; r<n; r++){
modify[l][r] = getModify(s.substring(l,r+1));
}
}
int[] f = modify[0];
for(int i=1;i<k;i++){
for(int j=n-1;j>i*2;j--){
f[j] = Integer.MAX_VALUE;
for(int L = i*2;L<j;L++){
f[j] = Math.min(f[j],f[L-1]+modify[L][j]);
}
}
}
return f[n-1];
}
private int getModify(String s){
char[] chs = s.toCharArray();
int n = chs.length;
int res = n;
for(int d : divisors[n]){
int cnt = 0;
for(int i0 = 0;i0 < d;i0++){
for(int i=i0,j=n-d+i0;i<j;i+=d,j-=d){
if(chs[i] != chs[j]){
cnt++;
}
}
}
res = Math.min(res,cnt);
}
return res;
}
}
这题直接给我干破防了。。。看题解都看了好一会,真的是史诗级巨坑。但是其中的部分思路很值得我们学习。
如果长度为 len
的字符串存在一个满足 1 <= d < len
的正整数 d
,len % d == 0
成立且所有对 d
做除法余数相同的下标对应的字符连起来得到的字符串都是 回文串 ,那么我们说这个字符串是 半回文串 。
根据半回纹串定义,我们可以知道,划分一个字符串时需要根据角标划分,而且是要用其除了1以外的公因数去划分,才能做到以上定义。根据题目要求中2 <= s.length <= 200,我们只用对200以内整数求公因数即可。这里我们用200维List存储公因数。
打表求公因数的时间复杂度太高,这里我们采用更加巧妙的方法:把除了1以外所有数从小到大先遍历一边,把所有公因数含有当前值的数的那一维List当中添加上这个数。当然,一个一个去找这个数是否含有该公因数还是暴力求解,所以我们直接从i*2开始(公因数不能是本身,因此含有该公因数的下一个元素就是2*i),然后向后以步长i进行遍历,这样遍历到的每一步都有该公因数。
Arrays.setAll(divisors,t->new LinkedList<>());
for(int i=1;i<mx;i++){
for(int j=i*2;j<mx;j+=i){
divisors[j].add(i);
}
}
然后我们定义一个getModify()函数,用来计算传入字符串转变为半回纹串最少需要改动几步。我们需要先得到传入字符串长度n,然后在divisor[ n ]处找到所有的公因数。然后就每个公因数找到修改次数,最终返回最少修改次数返回。假设本次遍历到的公因数为d,为了找到修改处,我们需要判断前后元素是否相同。所以我们让i0从0遍历到d,用来记录遍历步长内元素。同时,我们用双指针i,j分别代表前和后的指针位置,步长为d。i从i0的位置出发,j从n-d+i0出发,这样就能保证d长内每一个元素都能被遍历到,同时每次遍历正好是前与后。当chs[.i ] != chs[ j ]时,就是做出修改的时候,cnt++。最后在第一层循环内比较不同公因数得到的修改次数,返回最小值。
private int getModify(String s){
char[] chs = s.toCharArray();
int n = chs.length;
int res = n;
for(int d : divisors[n]){
int cnt = 0;
for(int i0 = 0;i0 < d;i0++){
for(int i=i0,j=n-d+i0;i<j;i+=d,j-=d){
if(chs[i] != chs[j]){
cnt++;
}
}
}
res = Math.min(res,cnt);
}
return res;
}
然后就是用备忘录方法,创建dp数组。200维,201列。dp数组modify[ i ][ j ]代表了从i到j处字符串的修改次数。
接下来就是重头戏了:
首先根据题目, 如果长度为 len
的字符串存在一个满足 1 <= d < len
的正整数 d
,len % d == 0
成立且所有对 d
做除法余数相同的下标对应的字符连起来得到的字符串都是 回文串,那么我们说这个字符串是 半回文串 。我们可以得知,半回纹串至少长度为2。因为分成了k个半回纹串,因此切了k-1刀。假设一刀下去分割了长度为m部分,那么就有原来是(n , k)切一刀变成(n-m , k-1)。因为总共切n-1刀,就遍历n-1次。从1开始遍历,后面要用。为了保证每个子字符串有大于等于2 的长度,我们在切每一刀时,从后向前遍历,最小为i*2。同时再用一个数L从前向后遍历,起始位置也是i*2。同时根据状态转移方程f[j] = Math.min(f[j],f[L-1]+modify[L][j]);最终返回f[n-1]即可
int[] f = modify[0];
for(int i=1;i<k;i++){
for(int j=n-1;j>i*2;j--){
f[j] = Integer.MAX_VALUE;
for(int L = i*2;L<j;L++){
f[j] = Math.min(f[j],f[L-1]+modify[L][j]);
}
}
}
return f[n-1];