来自0x3f【从周赛中学算法 - 2022 年周赛题目总结(下篇)】:https://leetcode.cn/circle/discuss/WR1MJP/
题目 | 难度 | 备注 |
---|---|---|
2507. 使用质因数之和替换后可以取到的最小值 | 1500 | 质因数分解 |
2470. 最小公倍数为 K 的子数组数目 | 1560 | LCM |
2447. 最大公因数等于 K 的子数组数目 | 1603 | GCD |
2344. 使数组可以被整除的最少删除次数 | 1641 | GCD |
2453. 摧毁一系列目标 | 1762 | 同余 |
2514. 统计同位异构字符串数目 | 2069 | 组合数学+逆元 |
2513. 最小化两个数组中的最大值 | 2302 | 二分答案+LCM+容斥 |
2338. 统计理想数组的数目 | 2615 | 质因数分解+放球问题 |
其他23:
题目 | 难度 | 备注 |
---|---|---|
2607. 使子数组元素和相等 | * | 中位数贪心+裴蜀定理 |
难度中等11
给你一个正整数 n
。
请你将 n
的值替换为 n
的 质因数 之和,重复这一过程。
n
能够被某个质因数多次整除,则在求和时,应当包含这个质因数同样次数。返回 n
可以取到的最小值。
示例 1:
输入:n = 15
输出:5
解释:最开始,n = 15 。
15 = 3 * 5 ,所以 n 替换为 3 + 5 = 8 。
8 = 2 * 2 * 2 ,所以 n 替换为 2 + 2 + 2 = 6 。
6 = 2 * 3 ,所以 n 替换为 2 + 3 = 5 。
5 是 n 可以取到的最小值。
示例 2:
输入:n = 3
输出:3
解释:最开始,n = 3 。
3 是 n 可以取到的最小值。
提示:
2 <= n <= 105
题解:https://leetcode.cn/problems/smallest-value-after-replacing-with-sum-of-prime-factors/solution/bao-li-by-endlesscheng-xh0b/
不断循环,计算n的质因数之和s,如果 s==n 说明无法再继续减小了,返回n,否则更新n为s,继续循环。
i
从小到大枚举**一定能够枚举所有质因数? 唯一分解定理举例:
i=4
时,此时前面i=2
已经整除过n
了,能够保证当i
能整除n
时,这个i
一定是质数
java:
class Solution {
/*
1. n怎么变?
a + n/a
2 + n/2
2. n是不会变大的
==> 不断循环,直到质因数之和 等于n 为止
*/
public int smallestValue(int n) {
while(true){
int x = n;
int sum = 0;
int i = 2;
//i从小到大枚举,如果大的i能整除n,小的i肯定能整除n,这是矛盾的
while(i * i <= x){
while(x % i == 0){
sum += i;
x /= i;
}
i++;
}
if(x > 1) sum += x; // x还有剩余,本身也是质数
if(sum == n) return n;
n = sum;
}
}
}
Go:
func smallestValue(n int) int {
res := -1
for res == -1 {
x := n
sum := 0
i := 2
for i * i <= n {
for x % i == 0 {
sum += i
x /= i
}
i++
}
if x > 1 {
sum += x
}
if sum == n {
res = n
}
n = sum
}
return res
}
难度中等25
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 nums
的 子数组 中满足 元素最小公倍数为 k
的子数组数目。
子数组 是数组中一个连续非空的元素序列。
数组的最小公倍数 是可被所有数组元素整除的最小正整数。
示例 1 :
输入:nums = [3,6,2,7,1], k = 6
输出:4
解释:以 6 为最小公倍数的子数组是:
- [3,6,2,7,1]
- [3,6,2,7,1]
- [3,6,2,7,1]
- [3,6,2,7,1]
示例 2 :
输入:nums = [3], k = 2
输出:0
解释:不存在以 2 为最小公倍数的子数组。
提示:
1 <= nums.length <= 1000
1 <= nums[i], k <= 1000
方法一:暴力
minlcm
要初始化为1?因为1和任何数x的最小公倍数都是xclass Solution {
/*
枚举+递推+剪枝:
nums[i,j]的最小公倍数=nums[i,j-1]的最小公倍数*nums[j]/gcd(nums[i,j-1]的最小公倍,nums[j])
即把nums[i,j-1]的最小公倍数看做一个整体
f[i,j]=f[i,j-1]*nums[j]/gcd(f[i,j-1],nums[j])
*/
public int subarrayLCM(int[] nums, int k) {
int n = nums.length;
int res = 0;
for(int i = 0; i < n; i++){
// 枚举起点
int min = 1; // min为nums[i,j]的最小公倍数
//为什么枚举时 min 要初始化为1?因为1和任何数x的最小公倍数都是x
for(int j = i; j < n; j++){
min = lcm(min, nums[j]); // 计算最小公倍数
if(min > k)
break; // 剪枝:前面超过了k,最小公倍数不可能为k
if(min == k) res++;
}
}
return res;
}
public int gcd(int x, int y){
return y == 0 ? x : gcd(y,x%y);
}
public int lcm(int x, int y){
return x*y / gcd(x, y);
}
}
go语言:
func subarrayLCM(nums []int, k int) int {
n := len(nums)
res := 0
for i := 0; i < n; i++ {
min := 1
for j := i; j < n; j++ {
min = lcm(min, nums[j])
if min > k { break}
if min == k {
res++
}
}
}
return res
}
func gcd(x, y int) int {
for x != 0 {
x, y = y%x, x
}
return y
}
func lcm(x, y int) int {
return x*y / gcd(x, y)
}
难度中等25
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 nums
的子数组中元素的最大公因数等于 k
的子数组数目。
子数组 是数组中一个连续的非空序列。
数组的最大公因数 是能整除数组中所有元素的最大整数。
示例 1:
输入:nums = [9,3,1,2,6,3], k = 3
输出:4
解释:nums 的子数组中,以 3 作为最大公因数的子数组如下:
- [9,3,1,2,6,3]
- [9,3,1,2,6,3]
- [9,3,1,2,6,3]
- [9,3,1,2,6,3]
示例 2:
输入:nums = [4], k = 7
输出:0
解释:不存在以 7 作为最大公因数的子数组。
提示:
1 <= nums.length <= 1000
1 <= nums[i], k <= 109
方法一:暴力
// 无优化暴力
class Solution {
public int subarrayGCD(int[] nums, int k) {
int n = nums.length;
int res = 0;
// 从i开始往后拓展子数组
for(int i = 0; i < n; i++){
int g = 0;
for(int j = i; j < n; j++){
g = gcd(g, nums[j]);
if(g == k) res++;
}
}
return res;
}
public int gcd(int x, int y){
return y == 0 ? x : gcd(y, x%y);
}
}
// 优化版本
class Solution {
public int subarrayGCD(int[] nums, int k) {
int n = nums.length;
int res = 0;
// 从i开始往后拓展子数组
for(int i = 0; i < n; i++){
// 如果nums[i]不是k的倍数,肯定没有k这个因数
if(nums[i] % k != 0) continue;
int g = 0;
for(int j = i; j < n; j++){
g = gcd(g, nums[j]);
if(g == k) res++;
}
}
return res;
}
public int gcd(int x, int y){
return y == 0 ? x : gcd(y, x%y);
}
}
难度困难14
给你两个正整数数组 nums
和 numsDivide
。你可以从 nums
中删除任意数目的元素。
请你返回使 nums
中 最小 元素可以整除 numsDivide
中所有元素的 最少 删除次数。如果无法得到这样的元素,返回 -1
。
如果 y % x == 0
,那么我们说整数 x
整除 y
。
示例 1:
输入:nums = [2,3,2,4,3], numsDivide = [9,6,9,3,15]
输出:2
解释:
[2,3,2,4,3] 中最小元素是 2 ,它无法整除 numsDivide 中所有元素。
我们从 nums 中删除 2 个大小为 2 的元素,得到 nums = [3,4,3] 。
[3,4,3] 中最小元素为 3 ,它可以整除 numsDivide 中所有元素。
可以证明 2 是最少删除次数。
示例 2:
输入:nums = [4,3,6], numsDivide = [8,2,6,10]
输出:-1
解释:
我们想 nums 中的最小元素可以整除 numsDivide 中的所有元素。
没有任何办法可以达到这一目的。
提示:
1 <= nums.length, numsDivide.length <= 105
1 <= nums[i], numsDivide[i] <= 109
居然自己想出来了,说明前面学的都是有用的!加油
class Solution {
// a 整除 b ==> b%a == 0 ==> a是b的公因数
public int minOperations(int[] nums, int[] numsDivide) {
// 计算target 数组的最大公因数 GCD
int g = 0;
for(int i = 0; i < numsDivide.length; i++){
g = gcd(g, numsDivide[i]);
}
//从小到大枚举nums,找到第一个与g gcd值=nums[i]的数
Arrays.sort(nums);
int i = 0;
while(i < nums.length){
int cur = gcd(nums[i], g);
// 最大公因数是nums[i],说明nums[i]和numsDivide的整体的最大公因数是nums[i],能被整除
if(cur == nums[i]) break;
i++;
}
if(i == nums.length) return -1;
return i;
}
public int gcd(int x, int y){
return y == 0 ? x : gcd(y, x%y);
}
}
难度中等9
给你一个下标从 0 开始的数组 nums
,它包含若干正整数,表示数轴上你需要摧毁的目标所在的位置。同时给你一个整数 space
。
你有一台机器可以摧毁目标。给机器 输入 nums[i]
,这台机器会摧毁所有位置在 nums[i] + c * space
的目标,其中 c
是任意非负整数。你想摧毁 nums
中 尽可能多 的目标。
请你返回在摧毁数目最多的前提下,nums[i]
的 最小值 。
示例 1:
输入:nums = [3,7,8,1,1,5], space = 2
输出:1
解释:如果我们输入 nums[3] ,我们可以摧毁位于 1,3,5,7,9,... 这些位置的目标。
这种情况下, 我们总共可以摧毁 5 个目标(除了 nums[2])。
没有办法摧毁多于 5 个目标,所以我们返回 nums[3] 。
示例 2:
输入:nums = [1,3,5,2,4,6], space = 2
输出:1
解释:输入 nums[0] 或者 nums[3] 都会摧毁 3 个目标。
没有办法摧毁多于 3 个目标。
由于 nums[0] 是最小的可以摧毁 3 个目标的整数,所以我们返回 1 。
示例 3:
输入:nums = [6,2,5], space = 100
输出:2
解释:无论我们输入哪个数字,都只能摧毁 1 个目标。输入的最小整数是 nums[1] 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 109
1 <= space <= 109
class Solution {
/*
预处理+枚举:
1.枚举每个数字,计算出对space取余后的结果,这个结果范围在[0,space-1]
2.只要取余后结果一致,那么必定存在一个最小值使得所有同余数的点被摧毁
3.只要找出出现次数最多的几个余数,然后再再这些余数之中找出对应的最小的nums[i]就是答案
时间复杂度:O(N) 空间复杂度:O(N)
*/
// 同余:如果(x-y) mod m = 0,那么称x与y对模m同余
public int destroyTargets(int[] nums, int space) {
// 1,3,5,7 % s(s=2) => 1 ==> 同余分组
Map<Integer, Integer> map = new HashMap<>(); // 统计每个余数出现次数(同余分组)
for(int i = 0; i < nums.length; i++){
map.put((nums[i] % space + space) % space, map.getOrDefault((nums[i] % space + space) % space, 0) + 1);
}
int res = 0, max = 0;
Arrays.sort(nums);
for(int i = 0; i < nums.length; i++){
int reminder = (nums[i] % space) % space; // 得到当前数的余数
if(map.getOrDefault(reminder, 0) > max){
res = nums[i];
max = map.get(reminder);
}
}
return res;
}
}
难度困难12
给你一个字符串 s
,它包含一个或者多个单词。单词之间用单个空格 ' '
隔开。
如果字符串 t
中第 i
个单词是 s
中第 i
个单词的一个 排列 ,那么我们称字符串 t
是字符串 s
的同位异构字符串。
"acb dfe"
是 "abc def"
的同位异构字符串,但是 "def cab"
和 "adc bef"
不是。请你返回 s
的同位异构字符串的数目,由于答案可能很大,请你将它对 109 + 7
取余 后返回。
示例 1:
输入:s = "too hot"
输出:18
解释:输入字符串的一些同位异构字符串为 "too hot" ,"oot hot" ,"oto toh" ,"too toh" 以及 "too oht" 。
示例 2:
输入:s = "aa"
输出:1
解释:输入字符串只有一个同位异构字符串。
提示:
1 <= s.length <= 105
s
只包含小写英文字母和空格 ' '
。题解:
视频讲解:https://www.bilibili.com/video/BV1Dd4y1h72z/
首先要明确,(a/b)%c=(a%c)/(b%c)
是不成立的,除法取模并不像加减乘那样可以直接分离,而要通过逆元计算
a / b(mod p) = a * pow(b,p-2) mod p
class Solution {
private static final int MOD = (int) 1e9 + 7;
// 首先,每个单词自己是互相独立的,因此分别计算每个单词的同位异构字符串的数目,再用乘法原理相乘
// 对于一个长为 n 的单词,其全排列的个数为 n!
// 但由于相同的字母不做区分,所以如果有x个字母a,还需要除以这些a的全排列的个数,即x!
// too => 3!/2! = 3
// hot => 3!/1! = 6
// 问题在于[3!/2!]怎么搞定除法? 费马小定理
// 代码实现时,分子分母可以分别计算,最后再用 费马小定理 相除
public int countAnagrams(String s) {
char[] c = s.toCharArray();
long ans = 1L, mul = 1L;
int[] cnt = new int[26];
for(int i = 0, j = 0; i < c.length; i++){
if(c[i] == ' '){
Arrays.fill(cnt, 0);
j = 0;
}else{
ans = ans * ++j % MOD;
mul = mul * ++cnt[c[i] - 'a'] % MOD;
}
}
return (int)(ans * pow(mul, MOD - 2) % MOD);
}
public long pow(long x, int n) {
long res = 1L;
for(; n > 0; n /= 2){
if(n % 2 > 0) res = res * x % MOD;
x = x * x % MOD;
}
return res;
}
}
// 费马小定理:a, p 互素, 且 p 为素数 a^(p-1) mod p = 1
// 记住公式:a / b(mod p) = a*pow(b,p-2)mod p
//
// 推导: (ans / mul) mod p = (ans / mul * 1) mod p
// = (ans / mul * mul ^ (p - 1)) mod p
// = (ans * mul ^ (p - 2)) mod p
来源:https://leetcode.cn/problems/count-anagrams/solution/c-by-code_learner-vwxz/
补充一下:
对于一个n长度的字符串,排列数计算。假设有n1个‘a’, n2个 ‘b’, n2个 ‘c’,那么生成的字符串的数量最终为:
(n1+n2+n3)!
------------- 【除号】 公式(1)
n1! * n2! *n3!
显然java代码中,A[i]计算的就是 i!
,那么g[i]计算的是什么呢,为什么要多一个g[i]计算? 因为我们计算的是形如
b
---- 【除号】
a
的形式,当a,b都比较大的时候,这时,你再来上一个%MOD,计算上有问题【具体为什么,我也不会,反正就是不能这么直接计算】。应该计算 b*a(-1)
【说明:b乘以a的-1次方】,这样计算再去%MOD才对【感觉意思就是%MOD对于除法不能直接用,嗯,先这样记着】。a(-1)%MOD【a的-1次方再对MOD取余】该怎么计算呢,很明显a(-1),会出现小数的。所以这里用到了费马小定理。
有两个数字,a(不是p的倍数),p(质数)。a的p-1方 和 1 对p同余
比如 a是3,p是5 ,那么a的p-1方是81,81%5 = 1。
再比如 a是4,p是3 ,那么a的p-1方是16,16%3 = 1。
因此,a^(-1)%MOD 【把它看作1】= a^(p-2)%MOD 【a^(p-2)是a的p-2次方】
总结一下:排列组合公式(1) 的计算,关键在于处理分母, a^(-1)%MOD = a^(p-2)%MOD
, 即得会转化为pow(a, p)
函数计算。剩下的就是洒洒水啦
class Solution {
private static final int MOD = (int) 1e9 + 7;
public int countAnagrams(String s) {
long a = 1, b = 1;
int[] cnt = new int[26];
for (String word : s.split(" ")) {
int n = word.length();
char[] chars = word.toCharArray();
Arrays.fill(cnt, 0);
for (int i = 0; i < n; i++) {
a = a * (i + 1) % MOD;
cnt[chars[i] - 'a']++;
b = b * cnt[chars[i] - 'a'] % MOD;
}
}
return (int) (a * qpow(b, MOD - 2) % MOD);
}
public long qpow(long x, int n) {
long res = 1L;
for(; n > 0; n /= 2){
if(n % 2 > 0) res = res * x % MOD;
x = x * x % MOD;
}
return res;
}
}
// too => 3!/2! = 3 ; hot => 3!/1! = 6
// 如何计算单词的排列?
// 例如too: a = 1; b = 1
// 第一次loop: a = a * 1 = 1 ; b = b * 1 = 1
// 第二次loop: a = a * 2 = 2 ; b = b * 1 = 1
// 第三次loop: a = a * 3 = 6 ; b = b * 2 = 2
// 最后too这个单词的排列 6/2
// 因为可能很大,要取余,而除法没有(a/b)%c=(a%c)/(b%c)
// 所以最后用费马小定理: a / b % MOD = a * (b^(MOD-2)) % MOD
难度中等26
给你两个数组 arr1
和 arr2
,它们一开始都是空的。你需要往它们中添加正整数,使它们满足以下条件:
arr1
包含 uniqueCnt1
个 互不相同 的正整数,每个整数都 不能 被 divisor1
整除 。arr2
包含 uniqueCnt2
个 互不相同 的正整数,每个整数都 不能 被 divisor2
整除 。arr1
和 arr2
中的元素 互不相同 。给你 divisor1
,divisor2
,uniqueCnt1
和 uniqueCnt2
,请你返回两个数组中 最大元素 的 最小值 。
示例 1:
输入:divisor1 = 2, divisor2 = 7, uniqueCnt1 = 1, uniqueCnt2 = 3
输出:4
解释:
我们可以把前 4 个自然数划分到 arr1 和 arr2 中。
arr1 = [1] 和 arr2 = [2,3,4] 。
可以看出两个数组都满足条件。
最大值是 4 ,所以返回 4 。
示例 2:
输入:divisor1 = 3, divisor2 = 5, uniqueCnt1 = 2, uniqueCnt2 = 1
输出:3
解释:
arr1 = [1,2] 和 arr2 = [3] 满足所有条件。
最大值是 3 ,所以返回 3 。
示例 3:
输入:divisor1 = 2, divisor2 = 4, uniqueCnt1 = 8, uniqueCnt2 = 2
输出:15
解释:
最终数组为 arr1 = [1,3,5,7,9,11,13,15] 和 arr2 = [2,6] 。
上述方案是满足所有条件的最优解。
提示:
2 <= divisor1, divisor2 <= 105
1 <= uniqueCnt1, uniqueCnt2 < 109
2 <= uniqueCnt1 + uniqueCnt2 <= 109
class Solution {
// 一看到「最大值的最小值」就想到二分答案。
// 推荐用 4 和 6 尝试
// d1 = 4, d2 = 6
// arr1 1 2 3 5 6 7 9 10 11 13
// arr2 1 2 3 4 5 7 8 9 10 11 13
// ==> 找到一些规律
// arr1 独占 6 的倍数,但又不是 4 的倍数(或者说不能是LCM(6, 4) = 12 的倍数)
// arr2 独占 4 的倍数,但又不是 6 的倍数(或者说不能是LCM(6, 4) = 12 的倍数)
// arr1 2 独享 既不是 4 的倍数, 也不是 6 的倍数
// = 所有数的个数 - (4 的倍数 + 6 的倍数 - 12 的倍数)
// 最大元素 的 最小值 : 二分答案(答案越大,能选的数越多,越能组成满足要求的arr1和arr2)
// 最坏情况下 divisor1, divisor2 = 2, 只能选奇数
public int minimizeSet(int divisor1, int divisor2, int uniqueCnt1, int uniqueCnt2) {
// divisor1 和 divisor2 的最大公倍数,它可能会造成 int 溢出,所以需要使用 long。
long g = lcm(divisor1, divisor2);
long left = 0;
long right =2 * ((long)uniqueCnt1 + (long)uniqueCnt2) - 1;
while(left <= right){
long mid = (left + right) / 2;
if(check(mid, g, divisor1, divisor2, uniqueCnt1, uniqueCnt2)) right = mid - 1;
else left = mid + 1;
}
return (int)left;
}
public boolean check(long x, long g, int d1, int d2, int u1, int u2){
long cnt1 = x - x / d1; // 不能被 divisor1 整除的数
long cnt2 = x - x / d2; // 不能被 divisor2 整除的数
long cnt3 = x - x / g; // 不能被 最大公倍数 整除的数
return cnt1 >= u1 &&
cnt2 >= u2 &&
cnt3 >= (u1 + u2); // 能选数的个数大于需要的数u1 和 u2 之和
}
public int gcd(int x, int y){
return y == 0 ? x : gcd(y, x%y);
}
public long lcm(int x, int y){
return (long)x * y / gcd(x, y);
}
}
难度困难49
给你两个整数 n
和 maxValue
,用于描述一个 理想数组 。
对于下标从 0 开始、长度为 n
的整数数组 arr
,如果满足以下条件,则认为该数组是一个 理想数组 :
arr[i]
都是从 1
到 maxValue
范围内的一个值,其中 0 <= i < n
。arr[i]
都可以被 arr[i - 1]
整除,其中 0 < i < n
。返回长度为 n
的 不同 理想数组的数目。由于答案可能很大,返回对 109 + 7
取余的结果。
示例 1:
输入:n = 2, maxValue = 5
输出:10
解释:存在以下理想数组:
- 以 1 开头的数组(5 个):[1,1]、[1,2]、[1,3]、[1,4]、[1,5]
- 以 2 开头的数组(2 个):[2,2]、[2,4]
- 以 3 开头的数组(1 个):[3,3]
- 以 4 开头的数组(1 个):[4,4]
- 以 5 开头的数组(1 个):[5,5]
共计 5 + 2 + 1 + 1 + 1 = 10 个不同理想数组。
示例 2:
输入:n = 5, maxValue = 3
输出:11
解释:存在以下理想数组:
- 以 1 开头的数组(9 个):
- 不含其他不同值(1 个):[1,1,1,1,1]
- 含一个不同值 2(4 个):[1,1,1,1,2], [1,1,1,2,2], [1,1,2,2,2], [1,2,2,2,2]
- 含一个不同值 3(4 个):[1,1,1,1,3], [1,1,1,3,3], [1,1,3,3,3], [1,3,3,3,3]
- 以 2 开头的数组(1 个):[2,2,2,2,2]
- 以 3 开头的数组(1 个):[3,3,3,3,3]
共计 9 + 1 + 1 = 11 个不同理想数组。
提示:
2 <= n <= 104
1 <= maxValue <= 104
class Solution:
def idealArrays(self, n: int, maxValue: int) -> int:
mod = 10 ** 9 + 7
max_k = 13
# n-1+13 +1
max_n = n + max_k
dp = [[0] * (max_k + 1) for _ in range(max_n)]
# 初始值,从0个位置中选出0个,只有一种方案
dp[0][0] = 1
# 预处理出 1 ~ n-1+13 的所有组合数
for i in range(1, max_n):
# 从任意个位置中选出0个,都只有一种方案
dp[i][0] = 1
for j in range(1, min(i, max_k) + 1):
dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j]) % mod
# 预处理 2 ~ maxValue 的所有数x,将这些数分解出所有的质因子,然后记录各个质因子的个数k。
# 由于并不关心个数k具体对应哪个质因子,所以使用数组记录个数k即可,而无需使用字典记录 个数k ——> 质因子
ks = [[] for _ in range(maxValue + 1)]
for i in range(2, maxValue + 1):
# 质因子p从2开始:2、3、5、7、……
p, x = 2, i
# 不用担心会记录质因子4的个数,质因子4的个数一定为0,因为能被4整除,就一定能被2整除,x先被2整除过若干次后,直到不能被2整除,
# 质因子p才会加1变成3,等质因子p加1变成4时,x都已经无法被2整除了,那就更不可能被4整除了,可理解为被2榨干了
while p * p <= x:
if x % p == 0:
k = 0
while x % p == 0:
k += 1
x //= p
ks[i].append(k)
p += 1
if x > 1:
# 若最后榨完的x还大于1,则说明最后剩余的x本身就是个质因子,该质因子的个数为1
ks[i].append(1)
res = 0
# 理想数组的结尾数字x可以为 [1, maxValue] 中的任意值
for x in range(1, maxValue + 1):
mul = 1
# x为1时,由于1不存在质因子,所以ks[1]为空数组。长度为n、结尾数字为1的理想数组只有1个,即 全1数组
for k in ks[x]:
mul = mul * dp[n - 1 + k][k] % mod
res = (res + mul) % mod
return res
"""
数论
分别考虑以x结尾长度为n的理想数组有多少个,数组结尾可以是1 ~ maxValue,因此把这些情况累加,就是最终结果。
以结尾为4、长度为5进行分析:
4的前面可以是4、2、1, 2的前面可以是2、1, 1的前面只能是1。例如:
[1, 2, 2, 4, 4]
[1, 1, 1, 4, 4]
[2, 2, 2, 4, 4]
[4, 4, 4, 4, 4]
以[1, 2, 2, 4, 4]为例,可以记为 [_, *2, _, *2, _],只需记录在哪些位置的元素发生了改变(倍增),
从当前倍增的位置开始 ~ 下一次倍增的位置之前(或数组结尾),将会一直维持这个值。
可假设每个数组的开头前面有一个值1,若数组中的第一个元素为1,则没有发生倍增;若第一个元素不是1,则发生了倍增。
例如:[2, 2, 2, 4, 4] 可表示为 [*2, _, _, *2, _];
在同一个位置可以发生多次倍增,例如:[1, 1, 1, 4, 4] 可表示为 [_, _, _, *2*2, _];[4, 4, 4, 4, 4] 可表示为 [*2*2, _, _, _, _]。
由于固定了结尾为4,而4的质因子为2、2,即 4 = (1) * 2 * 2
//
所有结尾为4、长度为5的理想数组,问题可转化为 结尾数字(4)的质因子可以放在哪些位置,当前有5个不同的位置,2个质因子2,
//
从5个位置中选择一个(将2个2放在一个位置)或两个(将2个2放在不同位置),因为2个2是相同的,谁先谁后,结果都是一样的。所以这是个组合问题。
//
问题进一步转化为:把k个相同的小球放进n个不同的盒子中,允许有些盒子为空,也允许一个盒子中放入多个小球,有多少种不同的放法?
//
该问题可用隔板法来求解,把n个盒子当做n-1个隔板,然后加上k个小球,相当于总共有 n-1 + k 个位置,从中选出n-1个位置放隔板,
即方案数为:C(n-1+k)(n-1)
由于maxValue <= 10^4,质因子最小为2,2^13 = 8192 < 10^4 < 16384 = 2^14,质因子越大,质因子的个数将会越小,
所以质因子为2时,质因子的个数k才能达到最大值13,即 k <= 13。所以上面的 C(n-1+k)(n-1) 可写为 C(n-1+k)(k) ,k 显然远小于n-1.
若结尾数字由多个不同的质因子组成,例如:k1个2、k2个3、k3个5,则可将问题分解为:
1、从n-1 + k1个位置中选出k1个位置放质因子2,得到 C(n-1+k1)(k1)
2、从n-1 + k2个位置中选出k2个位置放质因子3,得到 C(n-1+k2)(k2)
3、从n-1 + k3个位置中选出k3个位置放质因子5,得到 C(n-1+k3)(k3)
这3种情况之间互不影响:放质因子5的时候,不用关心这个位置之前放没放过2、3,以及放了多少个2、多少个3。
所以可采用乘法原理来计算最终结果:C(n-1+k1)(k1) * C(n-1+k2)(k2) * C(n-1+k3)(k3)
//
综上,原问题最终转化为:质因数分解出所有的质因子及其个数(其实只关注个数k) + 计算组合数问题
//
计算组合数问题 可用动态规划进行计算,假设dp[i][j] 表示从i个位置中选择j个,即 C(i)(j)。该问题可分为两种情况:
1、选择了位置i,则只需再从i-1个位置中选择j-1个,即 C(i-1)(j-1)
2、未选择位置i,则需要从i-1个位置中选择j个,即 C(i-1)(j)
所以,dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
"""
难度中等13
给你一个下标从 0 开始的整数数组 arr
和一个整数 k
。数组 arr
是一个循环数组。换句话说,数组中的最后一个元素的下一个元素是数组中的第一个元素,数组中第一个元素的前一个元素是数组中的最后一个元素。
你可以执行下述运算任意次:
arr
中任意一个元素,并使其值加上 1
或减去 1
。执行运算使每个长度为 k
的 子数组 的元素总和都相等,返回所需要的最少运算次数。
子数组 是数组的一个连续部分。
示例 1:
输入:arr = [1,4,1,3], k = 2
输出:1
解释:在下标为 1 的元素那里执行一次运算,使其等于 3 。
执行运算后,数组变为 [1,3,1,3] 。
- 0 处起始的子数组为 [1, 3] ,元素总和为 4
- 1 处起始的子数组为 [3, 1] ,元素总和为 4
- 2 处起始的子数组为 [1, 3] ,元素总和为 4
- 3 处起始的子数组为 [3, 1] ,元素总和为 4
示例 2:
输入:arr = [2,5,5,7], k = 3
输出:5
解释:在下标为 0 的元素那里执行三次运算,使其等于 5 。在下标为 3 的元素那里执行两次运算,使其等于 5 。
执行运算后,数组变为 [5,5,5,5] 。
- 0 处起始的子数组为 [5, 5, 5] ,元素总和为 15
- 1 处起始的子数组为 [5, 5, 5] ,元素总和为 15
- 2 处起始的子数组为 [5, 5, 5] ,元素总和为 15
- 3 处起始的子数组为 [5, 5, 5] ,元素总和为 15
提示:
1 <= k <= arr.length <= 105
1 <= arr[i] <= 109
class Solution {
// a[i] + a[i+1] + .. + a[i+k-1]
// = a[i+1] + a[i+2] + .. + a[i+k]
// ==> a[i] = a[i+k]
// 按照i mod k 的结果将 arr 分组,对每一组(记为b):
// 让数组b的所有元素相等的最少运算次数:
// 根据中位数贪心:将b的所有元素变为b的中位数是最优的
// 裴蜀定理 : 一个循环数组如果既有周期n,又有周期k,则必然有周期gcd(n,k)
public long makeSubKSumEqual(int[] arr, int k) {
int n = arr.length;
k = gcd(k, n);
long ans = 0;
for(int i = 0; i < k; i++){
List<Integer> list = new ArrayList<>();
for(int j = i; j < n; j += k){
list.add(arr[j]);
}
Collections.sort(list);
int mid = list.get(list.size() / 2);
for(int x : list){
ans += Math.abs(x - mid);
}
}
return ans;
}
public int gcd(int x, int y){
return y == 0 ? x : gcd(y, x%y);
}
}
提示:
1 <= k <= arr.length <= 105
1 <= arr[i] <= 109
class Solution {
// a[i] + a[i+1] + .. + a[i+k-1]
// = a[i+1] + a[i+2] + .. + a[i+k]
// ==> a[i] = a[i+k]
// 按照i mod k 的结果将 arr 分组,对每一组(记为b):
// 让数组b的所有元素相等的最少运算次数:
// 根据中位数贪心:将b的所有元素变为b的中位数是最优的
// 裴蜀定理 : 一个循环数组如果既有周期n,又有周期k,则必然有周期gcd(n,k)
public long makeSubKSumEqual(int[] arr, int k) {
int n = arr.length;
k = gcd(k, n);
long ans = 0;
for(int i = 0; i < k; i++){
List<Integer> list = new ArrayList<>();
for(int j = i; j < n; j += k){
list.add(arr[j]);
}
Collections.sort(list);
int mid = list.get(list.size() / 2);
for(int x : list){
ans += Math.abs(x - mid);
}
}
return ans;
}
public int gcd(int x, int y){
return y == 0 ? x : gcd(y, x%y);
}
}