一样的水

问题描述:有n个水桶,第i个水桶里面水的体积为$A_{i}$,你可以用1秒时间向一个桶里添加1体积的水。有q次询问,每次询问一个整数$p_{i}$,你需要求出使其中$p_{i}$个桶中水的体积相同所花费的最少时间。对于一次询问如果有多种方案,则采用使最终$p_{i}$个桶中水的体积最小的方案。
示例
输入:4,3,[1,2,3,4],[2,2,4]
返回值:[1,0,5]
说明:
第一次:花费一秒变为 2 2 3 4

第二次:已经存在两个水的体积一样的桶

第三次:花费五秒从2 2 3 4变为4 4 4 4

方法一

思路分析

     显然本题为了得到最小的操作时间以及最后最小的总体积,先将存储水的数组进行升序排序,然后对于每一次的操作有如下过程:
  • 首先每一次操作均会提供一个整数$p[i]$,用于表示需要使$p[i]$个桶中的水的体积相同,且时间最小
  • 从数组开始元素出发,设置滑动窗口,窗口的长度为$p[i]$,即使得$[0,1,...p[i]]$之间的水桶中的水的体积相同,$time=a[p[i]-1]*p[i]-(a[0]+a[1]…+a[p[i]])$
  • 接着窗口向后滑动一个长度,此时增加的元素为$a[s]$,此时的时间增量为增加的元素带来的时间增值$increase=(a[s]-a[s-1])*(len-1)$,而滑出去的最左边的元素所带来的减值$reduce=a[s-1]-s[s-p[i]]$
  • 该次操作的$time=time+increase-reduce$,不断的向后滑动窗口,将最小的$time$记录下来,并将改变的位置记录,循环结束后,改变时间最小的那一次需要增加的水的体积的元素
  • 接着进行下一次的操作过程。

图解



C++核心代码

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param n int整型 水桶的个数
     * @param q int整型 询问的次数
     * @param a int整型vector n个水桶中初始水的体积
     * @param p int整型vector 每次询问的值
     * @return int整型vector
     */
    vector<int> solve(int n, int q, vector<int>& a, vector<int>& p) {
        // write code here
        vector<int> res;
        sort(a.begin(),a.end());//排序
        //滑动窗口
        for(int i=0;i<q;i++)
        {
            
            int len=p[i];//滑动窗口的长度
            int ans=0;//标记下标
            int time=a[len-1]*len;
            for(int j=0;j<len;j++)
                time-=a[j];
            int min=time;//开始时的最少时间
            int increase=0;
            int reduce=0;
            for(int i=len;i<n;i++) 
            {
                increase=(a[i]-a[i-1])*(len-1);//向后滑动一个窗口增加的元素带来的时间增值
                reduce=a[i-1]-a[i-len];//滑出去的最左边的元素所带来的减值
                time+=increase-reduce;
                if(time<min)
                {
                    min=time;
                    ans=i-len+1;//记录需要改变的开始位置
                }               
            }  
            for(int i=ans;i<ans+len;i++)
                a[i]=a[ans+len-1];  //最少时间方案以及记录的开始元素位置调整桶的水的体积          
            res.push_back(min);
        }     
        return res;  
    }
};

复杂度分析

  • 时间复杂度:开始时需要对数组进行sort排序,时间复杂度为$O(n\log n)$。内外两层循环,外层表示需要操作的次数$q$,内层循环为滑动窗口的次数,最坏情况下滑动次数为数组的长度$n$,因此时间复杂度为$O(nq+n\log n)$
  • 空间复杂度:借助于一个大小为$q$的数组,因此空间复杂度为$O(q)$


方法二

思路分析

    根据方法一,对数组$a$求取前缀和,每一次的操作为如下过程:
  • 设置$dp[j]$表示数组元素中前$j-1$个的和
  • 每一次长度为$len$的数组元素需要添加的水为$a[j-1]*len-(a[j-1]+a[j-2] ..+ a[j – len-1])=a[j-1]*len-(dp[j] - dp[j - len])$

C++核心代码

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param n int整型 水桶的个数
     * @param q int整型 询问的次数
     * @param a int整型vector n个水桶中初始水的体积
     * @param p int整型vector 每次询问的值
     * @return int整型vector
     */
    vector<int> solve(int n, int q, vector<int>& a, vector<int>& p) {
        // write code here
        vector<int> res;
		vector<int> dp(n+1,0);
        sort(a.begin(),a.end());//排序
        for(int i=0;i<q;i++)//遍历操作数组
        {
			for(int j = 1; j <= n; j++)
                dp[j] = dp[j - 1] + a[j - 1]; //计算前缀和
            int len=p[i];//滑动窗口的长度
            int index=0;//标记下标
            int min=INT_MAX;//最少时间
			int time=0;
            for(int j=len;j<=n;j++) 
            {
                time=a[j-1]*len-(dp[j] - dp[j - len]);//长度为len需要增加的水
                if(time<min)
                {
                    min=time;
                    index=j;
                }               
            } 
            //结束之后  按照最少时间方案调整桶 
            for(int s=index-1;s>index-len;s--)
                a[s-1]=a[index-1];          
            res.push_back(min);
        }     
        return res;  
    }
};

复杂度分析

  • 时间复杂度:开始时需要对数组进行sort排序,时间复杂度为$O(n \log n)$。内外两层循环,外层表示需要操作的次数$q$,内层循环最坏情况下循环次数为数组的长度$n$,因此时间复杂度为$O(nq+n\log n)$
  • 空间复杂度:借助于一个大小为$q$的数组用于存储每次操作的结果,借助于一个大小为$n+1$的前缀和数组,因此空间复杂度为$O(n+q)$