贪心思想的本质,如何贪心地思考问题,例题+思路详解

2025牛客寒假算法基础集训营1 G 井然有序之衡
链接:题目链接

题目描述

小红拿到了一个数组,她可以进行任意次以下操作:选择两个元素,使得其中一个加 1,另一个减 1

小红希望最终数组变成一个排列,请你帮助她确定这能否实现。如果可以实现的话,还需要求出最小操作次数。

长度为 n n n 的排列是由 1 ∼ n 1 \sim n 1n n n n 个整数、按任意顺序组成的数组,其中每个整数恰好出现一次。例如, { 2 , 3 , 1 , 5 , 4 } \{2,3,1,5,4\} {2,3,1,5,4} 是一个长度为 5 5 5 的排列,而 { 1 , 2 , 2 } \{1,2,2\} {1,2,2} { 1 , 3 , 4 } \{1,3,4\} {1,3,4} 都不是排列,因为前者存在重复元素,后者包含了超出范围的数。

输入描述:

第一行输入一个整数 n ( 1 ≤ n ≤ 1 0 5 ) n(1 \leq n \leq 10^{5}) n(1n105) 代表数组中的元素数量。
第二行输入 n n n 个整数 a 1 , a 2 , ⋯   , a n ( − 1 0 9 ≤ a i ≤ 1 0 9 ) a_{1},a_{2},\cdots,a_{n}(-10^{9} \leq a_{i} \leq 10^{9}) a1,a2,,an(109ai109) 代表数组元素。

输出描述:

如果无法生成排列,直接输出 -1;否则,输出一个整数,代表最小的操作次数。

思路分析:

题意
给定数组并对数组进行任意次操作(任意两数一数加1一数减1),使其成为排列,求最小操作次数。
思路
首先,如果给定数组可以化为排列,即化为1~n,那么给定数组所有数和 ∑ i = 1 n a i = ( 1 + n ) ∗ n 2 \sum_{i=1}^{n}{a_i}= \frac{(1+n)*n}{2} i=1nai=2(1+n)n,反之,若数组和不满足上式,则一定无解。
若给定数组和满足上式,我们一定可以用给定操作将其化为排列。
原因如下
首先,我们根据此操作一定可以将任意数组 a n a_n an 化为{0,0,0,...,x}的形式,若有解则 x = ( 1 + n ) ∗ n 2 x=\frac{(1+n)*n}{2} x=2(1+n)n
其次,若有数组 a n a_n an满足{0,0,0,...,x}的形式,则我们可以想象将x不断减一,从左到右依次分给前方的0。最后一定是{1,2,3,...,n}的形式。
综上,可知任意数组若满足 ∑ i = 1 n a i = ( 1 + n ) ∗ n 2 \sum_{i=1}^{n}{a_i}= \frac{(1+n)*n}{2} i=1nai=2(1+n)n 用给定操作一定可以化为排列。
那么,我们可知能化为排列 ∑ i = 1 n a i = ( 1 + n ) ∗ n 2 \sum_{i=1}^{n}{a_i}= \frac{(1+n)*n}{2} i=1nai=2(1+n)n式 为等价关系,即判断数组是否满足此式即可。
最后,题目要求输出最少操作次数。对此我们贪心地考虑,原数组从小到大排序后,如果 a i a_i ai 都能化为 i i i ,所用操作次数应当是最少的。
虽然能猜到这样操作次数是最少的,但是为什么要这样贪心,为什么这样贪心是操作次数最少的呢?

我的分析如下:

首先,我们的目标是将一个数组化为一个排列,所需步数最少。我们可以考虑这个问题能否使其从大的问题化为相对小的问题。
我的想法是:从原数组最小的数字开始想,这个数字化为谁需要的步数是最少的呢?一定是排列中的1,那么我们是否可以将原问题转化为“将原数组最小的数字化为排列中最小的数字后,再将之后的数组化为剩下的排列,所需的步数最少。”,因为数组最小数字化为排列最小数字是局部的最优解,不一定能导向全局的最优解。所以对于这个转换,重点在于我们需要验证这个贪心策略持续进行后,是否能保证整个原数组化为排列所需步数最少。我们可以这样考虑:假设排列最小数字不是由数组最小数字化得,比如由数组次小数字化得(这里假设次小数字依然小于排列的最小数字),那么即使次小数字更接近排列的最小数字,之后数组的最小数字还是要经过相同的步数,先增加到次小数字化为的数字,再增加到你想要化成的数字。步数一定不优于最小数字直接化为排列最小数字。众所周知,其他策略一定不优 等价于 当前策略最优,那么我们就验证了这个贪心策略的全局最优性。
以上就是此贪心问题思考的流程,普遍来讲,贪心是将大问题不断用容易解决的小问题来缩小其规模,直到大问题化为足够小的问题,也就是从一般中发现特殊,再从特殊扩展到一般的想法。贪心问题思考上的倾向是注重特殊性问题,一般来说,普遍性的,不确定性的,没有特性的问题是难以处理的问题,而具有特殊性的问题是容易处理的问题,拿本题来讲,我在思考时会优先思考边界问题,因为边界自身具有特殊性,选择从最小数字的转化来思考(或者最大数字),便是注重特殊性问题,最后经过验证发现这个贪心策略是正确的。
另一方面,验证贪心策略最常用的方法就是反证法,假设我不采取当前贪心策略,而是其他策略(通常是选取一个具有普遍性的特例,如此题思路中的次小数字),通常会验证发现其导向的全局解一定不优于当前贪心策略导向的全局解,那么当前贪心得到的全局解一定是最优解。
相信各位读者能看出来,上述论断说的是其他策略一定不比当前策略更优,并没有说其他策略得不出全局最优解,所以能得出全局最优解的贪心策略可能不止一种,拿此题来讲,从最小数字思考和从最大数字思考都可以贪心得到全局最优解。
但如果发现其他策略可能优于当前贪心策略,那就要重新思考了,大概率是你的贪心策略考虑的太狭窄,也有可能此问题用简单的单一贪心策略无法得出最优解,需要用混合策略,或者动态规划等其他算法来解。对于什么时候用贪心,什么时候用动态规划这个问题,仁者见仁,智者见智,我之后也会以文章形式发出来讲一讲我的经验的。

样例代码

#include
using namespace std;
typedef long long ll;
const int N = 100005;
int num[N];
int main(){
    int n;
    ll sum = 0, ans = 0;
    scanf("%d",&n);
    for(int i = 1;i <= n;i ++){
        scanf("%d",&num[i]); sum += num[i];
    }
    if(sum != (1ll+n)*n/2) printf("-1");
    else{
        sort(num+1,num+1+n);
        for(int i = 1;i <= n;i ++){
            int t = num[i]-i;
            ans += t > 0 ? t : 0;
        }
        printf("%lld",ans);
    }
    return 0;
}

你可能感兴趣的:(ICPC,算法竞赛,贪心,贪心算法,算法)