[リートコード]-貪欲-1

序文

LeetCode をブラッシングする際に遭遇した貪欲アルゴリズムに関連する問題を記録する (最初の記事)

452. 最小限の矢で風船を爆発させる

間隔演算を見たとき、当初は差分を使用してそれを実行したいと思っていましたが、サンプル [[-2147483646,-2147483645], [2147483646, 2147483647]] があったため、テストを書いた後、配列が大きすぎることがわかりました。 。そこで配列の添え字の対応関係を調整できないか確認したいのですが、質問の意味を見ると各バルーン間隔の値は[-2 31 , 2 31 - 1 ]なっており、どう調整してもダメなようです。それでは、解決
を見てみましょう。

最も少ない矢印を使用するために、特定のバルーン i の xにあるように各矢印を貪欲に選択します。その後、後続のバルーンのx開始点がi のx終了点以下である限り、同じ矢で貫くことができる。したがって、最初に xの端で昇順に並べ替え、次に最初の xの端を最初の矢印の位置として選択し、次に逆方向に移動して、この矢印によって同時に貫通できる風船の数を確認します。最初の矢印の位置、バルーンの x端が次の矢印の位置として使用される、など

public int findMinArrowShots(int[][] points) {
    
    
    if (points.length == 0) {
    
    
        return 0;
    }
    Arrays.sort(points, new Comparator<int[]>() {
    
    
        public int compare(int[] point1, int[] point2) {
    
    
            //要升序排序,本来习惯写类似于 return o1.val - o2.val 来实现,这里由于样例中有出现
            //[[-2147483646,-2147483645],[2147483646,2147483647]] 这样的例子,加减法会溢出,所以只能通过比较来实现
            if (point1[1] > point2[1]) {
    
    
                return 1;
            } else if (point1[1] < point2[1]) {
    
    
                return -1;
            }
            return 0;
        }
    });
    int pos = points[0][1];
    int ans = 1;
    for (int[] balloon: points) {
    
    
        if (balloon[0] > pos) {
    
    
            pos = balloon[1];
            ans++;
        }
    }
    return ans;
}

55. ジャンプゲーム

最後の位置に到達できるかどうかを知るには、各ホップで到達できる最も遠い位置 maxFar を維持するだけでよく、maxFar が最後の位置以上であることが判明する限り、すぐに到達できると判断できます。最後の位置まで到達できる。したがって、最も遠い距離を貪欲に計算して維持するだけで済みます。

public boolean canJump(int[] nums) {
    
    
    int len = nums.length;
    int maxFar = 0;
    for (int i = 0; i < len; i++) {
    
    
    	//与跳跃游戏Ⅱ不同,这里有可能跳不到终点:如果当前位置已经超出了前面所能跳到的最远距离,就说明无法继续往后跳了
        if (i > maxFar) return false;
        maxFar = Math.max(maxFar, i + nums[i]); //维护最远距离
        if(maxFar >= len - 1) return true; //发现最远距离已经达到最后一个位置直接返回true
    }
    return true;
}

45. ジャンプゲームⅡ

nums[0] が x と等しいと仮定すると (x は 1 以上である必要があります)、最初のステップで飛び出すことができる範囲は [1,x] になります。これは、次のステップに進む必要があるという意味ではありません。この最初のステップで x にします。これは局所的な最適解ですが、必ずしも全体的な最適解であるとは限りません。

考えるべきことは、[1,x] は最初のジャンプがジャンプできる範囲であるということです。言い換えれば、最初のジャンプのターゲット位置はこの区間内の任意の位置であり得るということです。 2 番目のジャンプは、開始位置はこの間隔内の位置である必要がありますが、この間隔内の任意の位置にすることができます。

したがって、この区間の各位置 i について、1 ステップ後ろにジャンプすることで到達できる最も遠い位置、つまり i + nums[i] を計算できます。最大の結果は 2 番目のジャンプの位置ではありません。最遠距離? 正確には、1回目+2回目のジャンプでジャンプできる最も遠い位置です。可能ですが、最も遠いジャンプの具体的な開始位置を知る必要はなく、それが x (前のジャンプの最も遠い位置) よりも小さいことだけを知る必要があります。実際には、最初のジャンプの範囲が [1,x] であることを知る必要はありません。最も遠い位置が x であることだけを知る必要があります。座標 0 を 0 番目のジャンプの最も遠い位置とみなすことができます。最初のジャンプが特別じゃなくなるように

この戦略で計算すると、最終結果がグローバル最小ホップ数になることは明らかです。

貪欲という言葉は出てきませんが、実際、私たちのアルゴリズムでは各ホップの最大距離が非常に重要な考慮事項であり、貪欲の表れとも言えます。

なお、タイトルでは常に最終位置に到達できることを示しているため、途中にジャンプするとジャンプし続けられなくなる状況を考慮する必要はありません

public int jump(int[] nums) {
    
    
    int length = nums.length;
    int lastJumpMaxFar = 0; //记录上一跳所能到达的最远位置
    int maxPosition = 0; //维护更新下一跳所能到达的最远位置
    int steps = 0;       //记录最终步数
    for (int i = 0; i < length - 1; i++) {
    
    
        maxPosition = Math.max(maxPosition, i + nums[i]);
        //可以尝试提前跳出循环,可能不用遍历整个数组
        if(maxPosition >= length - 1) return steps + 1; 
        //已经跳到了上一跳所能到达的最远位置,还想往后的话,必须再跳一步,
        //且这一步所能跳到的最远距离就是 maxPosition
        if (i == lastJumpMaxFar) {
    
     
            lastJumpMaxFar = maxPosition;
            steps++;
        }
    }
    return steps;
}

1353. 参加できる会議の最大数

ある時点で参加できる一連の会議のうち、終了時間が最も小さいもの、つまり、
ある時点の日 (1 から開始、つまり初日) を貪欲に選択する必要があります。開始時刻が day 以下の一連の会議、終了時刻が day 以上の会議が存在する可能性があります。その場合、この時点でどの会議に出席するかについては、貪欲に 1 つを選択する必要があります。終了時間が長い会議ほど選択の余地が大きいためです。

各時点の選択は時点日の変更に応じて変化し続けるため、動的なメンテナンスが必要です

したがって、ヒープ/優先キューを使用する必要があり、終了時刻が最も小さいキューを選択する必要があります。そのため、会議の終了時刻を要素とする小さな上部ヒープがある場合は、最初にすべての会議を昇順で並べ替えます。
会議の開始時間に従って順序付けします。最初に開始時間に基づいて並べ替える必要があるためです。時間によって、1 日の特定の時点でどの会議に参加する可能性が高いかが決まります。これには、会議の開始時間が以下であることが必要です次に、
各日が 1 から列挙され、開始時刻が日以下であるすべての会議の終了時刻がヒープに追加され、終了を持つすべての会議の終了時刻がポップされます。ヒープから出た時間は 1 日未満です。残っているのは、開始時刻が日以下で、終了時刻が日以上であるすべての会議です。次に、その中で終了時刻が最も小さい会議 (この時点でのヒープの最上位要素) を選択します。時間を決めて、対応する会議に出席します。

public int maxEvents(int[][] events) {
    
    
    PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
    //按照会议开始时间
    Arrays.sort(events, (a,b) ->  a[0] - b[0]);
    int i = 0, res = 0, n = events.length;
    int day = 1;                            //day表示每一天
    while(i < n || !queue.isEmpty()){
    
    
        //将每个当前时间有可能进行的会议的结束时间添加到优先队列中
        while (i < n && events[i][0] <= day){
    
    
            queue.offer(events[i++][1]);
        }
        //到这里,队列中放的都是开始时间小于等于当前时间点的会议的结束时间
        //所以,排除掉结束时间小于当前时间点的会议
        while (queue.size() > 0 && queue.peek() < day){
    
    
            queue.poll();
        }
        //排除后,堆顶元素就是开始时间小于等于当前时间点,结束时间大于等于当前时间点,且结束时间最小的一个
        //所以选择这个会议去参加,这一步即为整个算法的 贪心 所在
        if (queue.size() > 0) {
    
    
            queue.poll();
            res++;
        }
        day++;
    }
    return res;
}

300. 最長の増加サブシーケンス

増加するサブシーケンスの場合、最後の要素が小さい場合、最後の要素よりも大きな数値が見つかる可能性が高くなります。これは、最後に見つかる最長の増加サブシーケンスが長くなることを意味します。

したがって、同じ長さの一連の最長増加サブシーケンスについては、最後の要素が最も小さいものを貪欲に選択し、配列 min を使用して、各長さの最長増加サブシーケンスの最後の要素、つまり min[i] を保存します。これは、長さが i の最も長く増加するサブシーケンスの最小の最後の要素であることを意味し、maxLen 変数を使用して、現在見つかっている最も長いサブシーケンスの長さを維持します。

次に、各配列要素 nums[i] を反復処理します。nums[i] が min[maxLen] より大きい場合、つまり、現在見つかっている最も長く増加するサブシーケンスの終了要素より大きい場合、自然に配列の長さを更新できます。最長のサブシーケンスを maxLen + 1 に設定し、この長さに対応する最後の要素を nums[i] に更新します

逆に、nums[i] が min[maxLen] 以下の場合、最も長く増加するサブシーケンスの長さを更新できないことを意味します。しかし、現時点では貪欲な操作を実行できます。nums[i] が現在の最も長く増加するサブシーケンスの終了要素以下であるため、次の最小終了を満たす maxLen 以下の長さを見つけることができます。長さ j の増加する部分列 要素は nums[i] より大きいですが、長さ j - 1 の増加する部分列の最小終了要素は nums[i] より小さいため、次の最小終了要素 min[j] を更新できます。長さ j から nums[i] までの増加する部分列

配列を走査した後、maxLen は最も長く増加するサブシーケンスの長さになります。

public int lengthOfLIS(int[] nums) {
    
    
    if (nums.length == 0) {
    
    
        return 0;
    }
    int maxLen = 1,numsLen = nums.length;
    //最长递增子序列的长度可以是nums.length,所以数组大小要为nums.length+1
    int[] min = new int[numsLen + 1];
    //初始化长度为1的递增子序列的末尾元素为nums[0]
    min[maxLen] = nums[0];
    for (int i = 1; i < numsLen; ++i) {
    
    
        if (nums[i] > min[maxLen]) {
    
    
            min[++maxLen] = nums[i];
        }else{
    
    
            //可以发现min数组是具有递增性的,所以可以用二分查找
            //如果找不到说明所有的数都比 nums[i] 大,此时要更新 min[1],所以这里将 pos 设为 0
            int l = 1, r = maxLen, pos = 0;
            while (l <= r) {
    
    
                int mid = (l + r) >> 1;
                if (min[mid] < nums[i]) {
    
    
                    pos = mid;
                    l = mid + 1;
                } else {
    
    
                    r = mid - 1;
                }
            }
            //找到j(即pos+1)满足 min[j] > nums[i] && min[j - 1] < nums[i]
            min[pos + 1] = nums[i];
        }
    }
    return maxLen;
}

貪欲+2点

1011. D 日以内に荷物を配達する能力

最適値問題では、Greedy + 2 点のアプローチが実現可能かどうかを優先します (誰が 2 点を拒否できますか?)
この問題では、1 日に配達される荷物の重量を 2 つの部分に分けます。 2 点の境界は1 日に配達される最小重量です。これはすべての荷物の中で最大の質量です。そうでない場合、最大の荷物は配達されません。2 点の正しい境界は、すべての荷物が 1 日に発送されることです。 , つまり、
1日に発送する荷物の最大数がmidである場合に、すべての荷物の重量が計算されます。dayが必要な日数です。dayがdays以下の場合、荷物の最大重量を意味します。 1 日に送る荷物の最大重量を増やす必要があることを意味します。l = Mid + 1 を更新します。なぜ Mid + 1 は Mid ではないのでしょうか。 , なぜなら、この時点のmidはもう条件を満たしていないので、後で考える必要はありません。day <= daysの場合、このmidが答えになる可能性があるため、mid - 1の代わりにr = Midとなります。

class Solution {
    
    
    public int shipWithinDays(int[] weights, int days) {
    
    
        int l = -1,r = 0,mid; //二分左边界是
        for(int i : weights){
    
    
            l = l > i ? l : i;
            r += i;
        }
        while(l < r){
    
    
            mid = (l + r) >> 1;
            int day = 1,oneDayWeights = 0;
            for(int i : weights){
    
    
                if(oneDayWeights + i > mid){
    
    
                    day++;
                    oneDayWeights = 0;
                }
                oneDayWeights += i;
            }
            if(day <= days){
    
    
                r = mid;
            }else{
    
    
                l = mid + 1;
            }
        }
        return l;
    }
}

410. 分割配列の最大値

k 個の機会内で最大の価値を見つけるこの種の問題は、貪欲 + 二分法で完了できるかどうかを検討できます。

単一の分割配列の合計。右側の境界は配列全体を分割セットとして扱い、対応する値は配列内の要素の合計になります。左側の境界は配列全体の各要素を分割セット、次に対応する分割セット それぞれの合計の最大値は、配列内の最大の要素の値です

左右の境界を決定したら、2 つの部分に分割します。2 つの点が分割されるたびに、分割された配列のそれぞれの合計の最大値として値が使用され、元の配列が次のように分割されます。分割の具体的な方法は、配列を走査し、走査された要素を現在の分割セットに追加することです。現在の分割セットの合計が最大値を超えた場合、分割数は 1 つ増加します。そして、現在の要素が新しい次の分割セットとして使用されます。最後に分割数とkの関係を判断します 分割数がkより大きい場合は最大値による分割では条件を満たさないことを意味し、最大値が小さすぎると故障の原因になります分割数が大きくなりすぎるのでそれぞれを合計する必要があります最大値が大きくなるのでleft =mid+1を更新しますただしmidに相当する解は条件を満たさないためmidではないことに注意してくださいしたがって、mid を含める必要はありません。分割数が k 以下であれば、実行可能な解を見つけることができますが、実行可能な解は複数ある可能性があります。条件を満たす最小の合計の最大値を見つける必要があります。条件を考慮しているため、より小さな方向に探索を続けて right =mid を更新する必要があります。ここでは、より小さな位置にある可能性があるため、mid を取得する必要があります。小さな範囲でより小さな実現可能な解決策が見つからない場合は、 mid は「保証された」ソリューションとして保持する必要があります。

public int splitArray(int[] nums, int k) {
    
    
    int left = 0,right = 0;
    for(int i : nums){
    
    
        if(left < i){
    
    
            left = i;
        }
        right += i;
    }
    int mid = 0,curCount = 1,sum = 0;
    while(left < right){
    
    
        mid = (left + right) >> 1;
        curCount = 1;
        sum = 0;
        for(int i : nums){
    
    
            if(sum + i > mid){
    
    
                sum = i;
                curCount++;
            }else{
    
    
                sum += i;
            }
        }
        if(curCount > k){
    
    
            left = mid + 1;
        }else{
    
    
            right = mid;
        }
    }
    return left;
}

おすすめ

転載: blog.csdn.net/Pacifica_/article/details/125259063