2607. 使子数组元素和相等

Posted lxy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2607. 使子数组元素和相等相关的知识,希望对你有一定的参考价值。

题目链接:2607. 使子数组元素和相等

方法:分组 + gcd + 中位数

解题思路

题意:将\\(arr\\)中某个元素\\(+1\\)\\(-1\\),使得任意长度为\\(k\\)的子数组的元素总和相等,且总操作数最少;
1、首先考虑数组\\(arr\\)为非循环数组:

  • 任意\\(k\\)长的子数组总和相等,则有下述情形,依次可以将\\(arr\\)分组,使得组内的元素相同,此时对组内元素操作时,为了使得操作次数最小,应该使得每个组内元素与组内的中位数相同,此时所需次数最少。
/*
      a[0] + a[1] + .. + a[k - 1]
    = a[1] + a[2] + ... + a[k] 
    = a[2] + a[3] + ... + a[k + 1]
    ...
    = a[n - k] + ... + a[n - 1]
=>
    a[0] = a[k] = a[2k] = ... = a[xk], xk < n
    a[1] = a[k + 1] = a[2k + 1] = ... = a[xk + 1], xk + 1 < n
    a[2] = a[k + 2] = a[2k + 2] = ... = a[xk + 2], xk + 2 < n
    ...
*/

2、考虑数组\\(arr\\)为循环数组:

  • 在上述的情况下,增加循环条件,每一组遍历到当前元素已经被添加过,因此需要有一个\\(isvisit\\)数组,判断当前元素是否已经被添加,然后使得每个组内元素与组内的中位数相同,输出总的操作次数。
/*
      a[0] + a[1] + .. + a[k - 1]
    = a[1] + a[2] + ... + a[k] 
    = a[2] + a[3] + ... + a[k + 1]
    ...
    = a[n - k] + ... + a[n - 1]
    = a[n - k + 1] + ... + a[n - 1] + a[0]
    = a[n - k + 2] + ... + a[n - 1] + a[0] + a[1]
    ...
=>
    a[0] = a[k] = a[2k] = ... = a[xk % n]
    a[1] = a[k + 1] = a[2k + 1] = ... = a[(xk + 1) % n]
    a[2] = a[k + 2] = a[2k + 2] = ... = a[(xk + 2) % n]
    ...
*/

代码

class Solution 
public:
    long long makeSubKSumEqual(vector<int>& arr, int k) 
        int n = arr.size();
        long long ans = 0;
        vector<int> isvisit(n); 
        for (int i = 0; i < k; i ++ ) 
            if (isvisit[i]) continue; // 被添加过则跳过
            vector<int> cur;
            int j = i;
            while (!isvisit[j])  // isvisit[j] = 1,表明回到某一个被添加过的元素,终止遍历
                isvisit[j] = 1;
                cur.push_back(arr[j]);
                j = (j + k) % n;
            
            sort(cur.begin(), cur.end());
            for (auto &num : cur) 
                ans += abs(num - cur[cur.size() / 2]);
            
            
        return ans;
    
;

复杂度分析

时间复杂度:\\(O(nlogn)\\)
空间复杂度:\\(O(n)\\)

优化

  • 裴蜀定理简介
    \\(a,b\\)是整数,且\\(gcd(a,b)=d\\),那么对于任意的整数\\(x,y\\)\\(ax+by\\)都一定是\\(d\\)的倍数,特别地,一定存在整数\\(x,y\\),使\\(ax+by=d\\)成立。

由于本题为循环数组,所以周期除了\\(k\\)还有\\(n\\),那么对于\\(a[i]\\)有,\\(a[i] = a[i + xk + yn]\\)。根据上述定理,有\\(a[i] = a[i + gcd(k, n)]\\),那么只需要判断。

/*
    k = gdc(k, n)
    a[0] = a[k] = a[2k] = ... = a[xk], xk < n
    a[1] = a[k + 1] = a[2k + 1] = ... = a[xk + 1], xk + 1 < n
    a[2] = a[k + 2] = a[2k + 2] = ... = a[xk + 2], xk + 2 < n
    ...
*/
class Solution 
public:
    long long makeSubKSumEqual(vector<int>& arr, int k) 
        int n = arr.size();
        long long ans = 0;
        k = gcd(n, k);
        for (int i = 0; i < k; i ++ ) 
            vector<int> cur;
            for (int j = i; j < n; j += k) cur.push_back(arr[j]);
            nth_element(cur.begin(), cur.begin() + cur.size() / 2, cur.end()); // 快速选择降低复杂度
            for (auto &num : cur) 
                ans += abs(num - cur[cur.size() / 2]);
            
            
        return ans;
    
;

复杂度分析

时间复杂度:\\(O(n)\\)
空间复杂度:\\(O(n)\\)

最小化操作次数使数组的所有元素相等

【中文标题】最小化操作次数使数组的所有元素相等【英文标题】:Minimize the number of operation to make all elements of array equal 【发布时间】:2016-02-13 13:03:52 【问题描述】:

给定一个包含 n 个元素的数组,您只能执行 2 种操作来使数组的所有元素相等。

    将任意元素乘以 2 将元素除以 2(整数除法)

你的任务是最小化上述操作的总数,以使数组的所有元素相等。

例子

array = [3,6,7] 最小运算是 2,因为 6 和 7 可以除以 2 得到 3。

我什至想不出蛮力解决方案。

约束 1

【问题讨论】:

可能会提到约束 别忘了发布约束,这很重要! 蛮力也可以很快 【参考方案1】:
    通过二进制扩展将所有数字视为01 的字符串。

例如:3, 6, 7 分别表示为 11, 110, 111

    除以2相当于去掉最右边的01,乘以2相当于从右边加上一个0

对于由01 组成的字符串,让我们将它的“头”定义为一个子字符串,该子字符串是该字符串的左几项,以1 结尾。 例如:1100101 有头像111110011100101

    任务变为查找所有给定字符串中最长的公共头部,然后确定在该公共头部之后添加多少个0

一个例子:

假设您有以下字符串:

10101001101011101111010001

    找出10101001101011的最长公共头,即10101; 找出1010110111的最长公共头,即101; 找出1011010001的最长公共头,即101

那么你确定所有的数字都应该变成101 00...形式的数字。

要确定在101 之后要添加多少个0,请在每个字符串中找到紧跟101 的连续0 的数量:

对于10101001:1

对于101011:1

对于10111:0

对于1010001:3

仍然需要找到一个整数 k 来最小化 |k - 1| + |k - 1| + |k - 0| + |k - 3|。在这里我们找到k = 1。所以每个数字最后都应该是1010

【讨论】:

是的,这是正确的解决方案。一旦你观察到解的形状(前缀后跟零),这真的很容易。 @WhatsUp 如何有效地找到k? 鉴于您的限制,甚至可以通过暴力破解:尝试从020 的每个k,比如说。 我已经用这个实现替换了我的答案。我已经通过蛮力做到了,如果这还不够快,那么您可以在前缀之后将每个数字 0 的数字分组,因为这是唯一重要的事情并将其转化为一个因素。 这个答案是否符合问题的要求?只允许两种类型的操作;乘以二除以二。二进制展开将需要更多操作,并且诸如加法或减法之类的额外计算也将超出初始问题要求的范围。结果是正确的,但旅程似乎越界了。也许我误读了这个问题?【参考方案2】:

正如另一个答案所解释的,不需要回溯。为了好玩,对这种方法进行了一点实现。 (见底部在线运行链接):

首先我们需要一个函数来确定数字中的二进制位数:

    def getLength(i: Int): Int = 
         @annotation.tailrec
         def rec(i: Int, result: Int): Int =
           if(i > 0)
             rec(i >> 1, result + 1)
           else
             result
         rec(i, 0)
   

那么我们需要一个函数来判断两个等长数的公共前缀

   @annotation.tailrec
   def getPrefix(i: Int, j: Int): Int =
         if(i == j) i
         else getPrefix(i >> 1, j >> 1)

一个任意数字的列表:

   def getPrefix(is: List[Int]): Int = is.reduce((x,y) => 
         val shift = Math.abs(getLength(x) - getLength(y))
         val x2 = Math.max(x,y)
         val y2 = Math.min(x,y)
         getPrefix((x2 >> shift), y2)
   )

那么我们需要后缀的长度而不计算后缀的零:

    def getSuffixLength(i: Int, prefix: Int) = 
         val suffix = i ^ (prefix << (getLength(i) - getLength(prefix)))
         getLength(suffix)
    

现在我们可以计算将操作 i 与附加“零”零的前缀同步所需的操作数。

    def getOperations(i: Int, prefix: Int, zeros: Int): Int = 
         val length = getLength(i) - getLength(prefix)
         val suffixLength = getSuffixLength(i, prefix) 
         suffixLength + Math.abs(zeros - length + suffixLength)
    

现在我们可以找到最少的操作数并将其与我们将同步到的值一起返回:

    def getMinOperations(is: List[Int]) = 
        val prefix = getPrefix(is)
        val maxZeros = getLength(is.max) - getLength(prefix)
        (0 to maxZeros).mapzeros => (is.mapgetOperations(_, prefix, zeros).sum, prefix << zeros).minBy(_._1)
    

您可以在以下位置尝试此解决方案:

http://goo.gl/lLr5jl

找到正确数量的零的最后一步可以改进,因为只有没有前导零的后缀的长度很重要,而不是它的样子。所以我们可以通过计算有多少来计算我们需要的操作数:

    def getSuffixLength(i: Int, prefix: Int) = 
         val suffix = i ^ (prefix << (getLength(i) - getLength(prefix)))
         getLength(suffix)
    

    def getMinOperations(is: List[Int]) = 
        val prefix = getPrefix(is)
        val maxZeros = getLength(is.max) - getLength(prefix)
        val baseCosts = is.map(getSuffixLength(_,prefix)).sum
        val suffixLengths: List[(Int, Int)] = is.foldLeft(Map[Int, Int]())
            case (m,i) => 
                val x = getSuffixLength(i,prefix) - getLength(i) + getLength(prefix)
                m.updated(x, 1 + m.getOrElse(x, 0))
            
        .toList
        val (minOp, minSol) = (0 to maxZeros).mapzeros => (suffixLengths.map
           case (x, count) => count * Math.abs(zeros + x)
        .sum, prefix << zeros).minBy(_._1)
        (minOp + baseCosts, minSol)
    

所有腋窝手术只需要最大数量大小的对数时间。我们必须通过孔列表来收集后缀长度。然后我们必须猜测在最大数量的许多零中最多有对数的零的数量。所以我们应该有一个复杂度

O(|list|*ld(maxNum) + (ld(maxNum))^2)

所以对于你的界限,这基本上是输入大小的线性。

这个版本可以在这里找到:

http://goo.gl/ijzYik

【讨论】:

以上是关于2607. 使子数组元素和相等的主要内容,如果未能解决你的问题,请参考以下文章

2021-06-28:最接近目标值的子序列和。给你一个整数数组 nums 和一个目标值 goal 。你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal 。也就是说,如果子序列元素和

可视化两个数值数组之间的差异

如何垂直居中一个元素

当父元素和子元素都具有不同的百分比宽度时如何使子 div 居中

453. 最小移动次数使数组元素相等(数学)

最小化操作次数使数组的所有元素相等