最も理解しやすい貪欲なアルゴリズム

1. アルゴリズムの説明

名前が示すように、貪欲アルゴリズムまたは貪欲思考は、各操作が局所的に最適であることを保証する貪欲戦略を採用し、最終結果が全体的に最適になるようにします。


簡単な例を挙げると、シャオミンとシャオ・ワンはリンゴを食べるのが好きで、シャオミンはリンゴを5個食べることができ、シャオ・ワンはリンゴを3個食べることができます。リンゴ園にはリンゴが無限にあることが知られています。シャオミンとシャオワンにリンゴを最大何個食べられるか尋ねてください。


この例では、私たちが選択できる貪欲な戦略は、各人が食べられる最大数のリンゴを食べるというもので、これは各人にとって局所的に最適です。また、グローバルな結果はローカルな結果の単純な合計であり、ローカルな結果は互いに無関係であるため、ローカルな最適な戦略はグローバルな最適な戦略でもあります。


2. 流通上の問題

2.1. Cookieの配布

2.1.1、トピックの説明

455. クッキーを配る

あなたは素晴らしい親で、子供たちに小さなクッキーをあげたいと思っているとしましょう。ただし、各子供に与えられる Cookie は 1 つだけです。

各子供には、子供の食欲を満たすことができるビスケットの最小サイズであるi食欲値があり、各ビスケットにはサイズがありますこのクッキーを子供に配布できれ、子供は満足するでしょう。あなたの目標は、できるだけ多くの子供たちを満足させ、この最大値を出力することです。g[i]js[j]s[j] >= g[i]ji

2.1.2. 入出力例

例 1:
入力: g = [1, 2, 3]、s = [1, 1]
出力: 1
説明:
3 人の子供と 2 つのビスケットがあり、3 人の子供の食欲の値は次のとおりです: 1、2 、3.
小さなビスケットが 2 つありますが、どちらもサイズが 1 なので、食欲値 1 の子供しか満足できません。
したがって、1を出力する必要があります。

例 2:
入力: g = [1, 2]、s = [1, 2, 3]
出力: 2
説明:
2 人の子供と 3 つのビスケットがあり、2 人の子供の食欲値はそれぞれ 1 と 2 です。 。
量も大きさも十分なクッキーがあり、子供たち全員が満足できます。
したがって、2を出力する必要があります。

2.1.3. 解決策

最も空腹度の低い子が満腹になる可能性が最も高いため、この子を最初に考慮します。残ったビスケットでお腹が空いている子どもをできるだけ満足させるためには、子どもの空腹以上の最小サイズのビスケットを与える必要があります。この子を満足させた後、同じ戦略を採用し、満足のいくクッキーがなくなるまで、残りの子の中で最も空腹度の低い子を検討します。


つまり、ここでの貪欲な戦略は、残りの子供たちの中で最も空腹度の低い子供に、最小の完全なビスケットを割り当てることです。具体的な実装としては、サイズ関係を取得する必要があるため、子とCookieを別々にソートするのが便利です。このようにして、空腹度が最も小さい子供とサイズが最も小さいビスケットから始めて、何組が条件を満たすことができるかを計算できます。

class Solution {
    
    
    public int findContentChildren(int[] g, int[] s) {
    
    
        Arrays.sort(g);
        Arrays.sort(s);
        int child = 0, cookie = 0;
        // 优先给最小饥饿度的孩子分配最小的饼干
        while (child < g.length && cookie < s.length) {
    
    
            if (g[child] <= s[cookie]) {
    
    
                child++;
            }
            cookie++;
        }
        return child;
    }
}

複雑さの分析

  • 時間計算量: O(mlogm+nlogn)、ここで m と n はそれぞれ配列 g と s の長さです。2 つの配列のソートの時間計算量は O(mlogm + nlogn)、配列の走査の時間計算量は O(m+n) であるため、合計の時間計算量は O(mlogm + nlogn) です。
  • 空間計算量: O(logm+logn)、ここで m と n はそれぞれ配列 g と s の長さです。スペースの複雑さは主に、並べ替えに伴う余分なスペースのオーバーヘッドです。

2.2. キャンディーの配布

2.2.1、トピックの説明

135. お菓子を配る

n子供たちは一列に並びます。各子供の評価を表す整数の配列が与えられますratings

次の要件に従って、これらの子供たちにキャンディーを配布する必要があります。

  • 各子供には少なくとも1キャンディーが割り当てられます。
  • 隣接する 2 人の子供の中でより高いスコアを持つ子供がより多くのキャンディーを獲得します。

各お子様にキャンディーを配布し、準備する必要がある最小限のキャンディーの数を計算して返してください。

2.2.2. 入出力例

例 1:
入力: ratings = [1, 0, 2]
出力: 5
説明: 1 番目、2 番目、3 番目の子に、それぞれ 2 個、1 個、2 個のキャンディーを配布できます。

例 2:
入力: ratings = [1, 2, 2]
出力: 4
説明: 1 番目、2 番目、3 番目の子にそれぞれ 1、2、1 個のキャンディーを配布できます。
    3 番目の子供は、タイトルの 2 つの条件を満たすキャンディーを 1 つだけ獲得します。

2.2.3. 解決策

このトピックを実行した後2.1、分发饼干、比較関係のある貪欲な戦略を分類または選択する必要があると思いますか? この質問も貪欲な戦略を使用していますが、必要なのは 2 つの単純なトラバースだけです: すべての子のキャンディーの数を 1 に初期化する; 右側の子のスコアが左側の子よりも高い場合は、最初に左から右にトラバースします。 , 次に、右側の子のキャンディーの数が、左側の子のキャンディーの数に 1 を加えた値に更新されます。次に、左側の子のスコアが右側の子供のスコアよりも高い場合は、右から左にトラバースします。左の子の現在のキャンディーの数が右の子のキャンディーの数より大きくない場合、左の子のキャンディーの数は、右の子のキャンディーの数に 1 を加えたものに更新されます。これら 2 つの走査を通じて、割り当てられたキャンディーはトピックの要件を満たすことができます。ここでの貪欲な戦略は、各トラバースで、隣接する側のサイズ関係のみが考慮され、更新されるということです。


同様示例1に、キャンディー分布を [1,1,1] として初期化し、最初の走査更新後の結果は [1,1,2] 、2 回目の走査更新後の結果は [2,1,2] になります。

class Solution {
    
    
    public int candy(int[] ratings) {
    
    
        // 每个孩子至少一颗糖果
        int candyCount = ratings.length;
        // 用于记录每个孩子需要评分需要额外奖励的糖果数量
        int[] candyArr = new int[ratings.length];
        
        // 从左往右遍历一遍,如果右边孩子的评分比左边的高,则右边孩子的糖果数更新为左边孩子的糖果数加 1
        for (int i = 1; i < ratings.length; i++) {
    
    
            if (ratings[i] > ratings[i - 1]) {
    
    
                candyArr[i] = candyArr[i - 1] + 1;
                candyCount += candyArr[i];
            }
        }
        
        /*
         * 从右往左遍历一遍,如果左边孩子的评分比右边的高,
         * 且左边孩子当前的糖果数不大于右边孩子的糖果数(排除上面从左往右遍历过程中已追加的糖果数量影响),
         * 则左边孩子的糖果数更新为右边孩子的糖果数加 1
         */
        for (int i = ratings.length - 2; i >= 0; i--) {
    
    
            if (ratings[i] > ratings[i + 1] && candyArr[i] <= candyArr[i + 1]) {
    
    
                // 因该位置孩子的糖果数量较上次遍历又发生了变化,会导致重复计算,故需减去之前的数量,重新计算
                candyCount -= candyArr[i];
                candyArr[i] = candyArr[i + 1] + 1;
                candyCount += candyArr[i];
            }
        }
        return candyCount;
    }
}

複雑さの分析

  • 時間計算量: O(n)、ここで n は子の数です。左から右への走査と右から左への走査をそれぞれ満たすキャンディーの最小数を計算するには、配列を 2 回走査する必要があります。
  • 空間計算量: O(n)、ここで n は子の数です。2 回の横断中に各子供に与えられるキャンディーの数を保存する必要があります。

3. 間隔の問題

3.1. 重複しない間隔

3.1.1、トピックの説明

435. 重複しない間隔 間隔

の集合が与えられるとしintervalsますintervals[i] = [starti, endi]残りの間隔が重ならないように削除する必要がある間隔の最小数を返します。

3.1.2. 入出力の例

例 1:
入力:間隔 = [ [1, 2], [2, 3], [3, 4], [1, 3] ] 出力
: 1
説明: [1,3] を除いた残りの間隔は、重複はありません。

例 2:
入力: Interval = [ [1, 2], [1, 2], [1, 2] ]
出力: 2
説明:残りの間隔が重複しないようにするには、2 つの [1,2] を削除する必要があります。

例 3:
入力:間隔 = [ [1, 2], [2, 3] ]
出力: 0
説明:間隔はすでに重複していないため、削除する必要はありません。

3.1.3、問題の解決策

保持する間隔を選択する場合、間隔の終わりは非常に重要です。選択した間隔の終わりが小さいほど、他の間隔のためのスペースが多く残され、より多くの間隔を保存できます。したがって、私たちが採用する貪欲な戦略は、小さくてバラバラな終わりの間隔を維持することを優先することです。


具体的な実装方法としては、まず端の大きさに応じて区間を昇順にソートし、その都度、前回選択した区間と重ならない、端が最も小さい区間を選択する。ここでは Java の Lambda がカスタム並べ替えに使用されています。


では示例1、ソートされた配列は [[1,2], [2,3], [1,3], [3,4]] です。貪欲な戦略によれば、まず区間 [1,2] に初期化します。次に、[2,3] は [1,2] と交差せず、それを維持します。これは、[1,3] が [2,3] と交差するためです。 、間隔をスキップします。[3,4] は [2,3] と交差しないため、間隔は維持されます。したがって、最終的な予約間隔は [[1,2], [2,3], [3,4]] となります。

class Solution {
    
    
    public int eraseOverlapIntervals(int[][] intervals) {
    
    
        if (intervals.length <= 0) {
    
    
            return 0;
        }
        // 将区间按照结尾的大小进行增序排序
        List<int[]> intervalList = Arrays.stream(intervals).sorted(Comparator.comparingInt(interval -> interval[1])).collect(Collectors.toList());
        int ans = 0;
        for (int preIndex = 0, i = 1; i < intervalList.size(); i++) {
    
    
            // 比较后一个区间开始是否在前一个区间内即可
            if (intervalList.get(i)[0] < intervalList.get(preIndex)[1]) {
    
    
                ans++;
                continue;
            }
            preIndex = i;
        }
        return ans;
    }
}

複雑さの分析

  • 時間計算量: O(nlogn)、n は間隔の数です。すべての間隔を右端点で昇順に並べ替えるのに O(nlogn) 時間、横断するのに O(n) 時間が必要です。前者は後者より漸近的に大きくなるため、合計の時間計算量は O(nlogn) になります。
  • スペース複雑度: O(logn)、ソートに必要なスタックスペースです。

4. 練習する

4.1. 基本的な難易度

4.1.1. 花を植える問題

4.1.1.1、トピックの説明

605. 花を植える問題

非常に長い花壇があり、一部の区画には花が植えられているが、他の部分には植えられていないとします。ただし、隣接する区画に花を植えることはできず、水をめぐって競合し、どちらも枯れてしまいます。

花壇を表す整数配列花壇が与えられます。これは 0 と 1 の数値で構成されます。0 は花が植えられていないことを意味し、1 は花が植えられていることを意味します。別の数 n がありますが、植え付けルールに違反せずに n 個の花を植えることはできますか? はいの場合は true、いいえの場合は false を返します。

4.1.1.2、入力と出力の例

例 1:
入力:花壇 = [1, 0, 0, 0, 1]、n = 1
出力: true

例 2:
入力:花壇 = [1, 0, 0, 0, 1]、n = 2
出力: false

4.1.1.3、問題の解決策

欲張りな観点から言えば、植え付けルールに違反しない範囲でできるだけ多くの花を植えて、植えることができる花の最大数が n 以上であるかどうかを判断し、花を何本植えるかを判断します。植えられる花の数が n に達した場合、配列の残りの計算を続行せずに直接返すことができます。

class Solution {
    
    
    public boolean canPlaceFlowers(int[] flowerbed, int n) {
    
    
        for (int i = 0; i < flowerbed.length && n > 0; i++) {
    
    
            if (flowerbed[i] == 0 && (i == 0 || flowerbed[i - 1] == 0)
                    && (i == flowerbed.length - 1 || flowerbed[i + 1] == 0)) {
    
    
                n--;
                flowerbed[i] = 1;
            }
        }
        return n == 0;
    }
}

複雑さの分析

  • 時間計算量: O(n)、ここで n は配列の花壇の長さです。配列を 1 回走査する必要があります。
  • 空間複雑度: O(1)使用される追加スペースは一定です。

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

4.1.2.1、トピックの説明

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

XY 平面で表される壁に球形の風船が取り付けられています。壁上のバルーンは整数配列 で記録されますpoints。 ここで、は との間の水平直径を持つバルーンpoints[i] = [xstart, xend]を表します。バルーンの正確な y 座標はわかりません。弓矢は、X 軸に沿ったさまざまな点から完全に垂直に射ることができます。座標 x に矢を放ち、直径が座標 で始まり で終わり、を満たす風船がある場合、その風船は爆発します。射出できる弓の数に制限はありません弓矢は一度放たれると無限に前進することができます。配列を指定して、すべての風船を破裂させるために発射する必要がある矢の最小数を返します。xstartxend

xstartxendxstart ≤ x ≤ xend

points

4.1.2.2、入力と出力の例

例 1:
入力: Points = [ [10, 16], [2, 8], [1, 6], [7, 12] ] 出力: 2
説明:
風船は 2 本の矢印で破裂できます:
○ at x = 6か所から矢を放ち、風船[2,8][1,6]を割ります。
○ x = 11 の位置に矢を放ち、風船 [10,16] と [7,12] を割ります。

例 2:
入力:ポイント = [ [1, 2], [3, 4], [5, 6], [7, 8] ] 出力: 4 説明
:
風船は矢を放つ必要があり、合計 4 つが矢を射る必要があります。必要な矢印。

例 3:
入力: Points = [ [1, 2], [2, 3], [3, 4], [4, 5] ] 出力
: 2
説明:風船は 2 つの矢印で破裂できます:
x = で ○ 2か所に矢を放ち、風船[1,2]と[2,3]を割ります。
○ x = 4 に矢を放ち、風船[3,4]と[4,5]を割ります。

4.1.2.3、問題の解決策

この質問は3.1、无重叠区间と非常によく似ていますが、これも貪欲な戦略を採用しています。まず端の大きさに応じて間隔を昇順に並べ替え、次に風船を爆発させた弓矢が右端の境界に移動するかどうかを確認します。次のインターバルバルーンを爆発させることができます。


具体的な実装方法としては、まず端の大きさに応じて区間を昇順にソートすることになりますが、カスタムソートにはJava Lambdaを利用します。次に、最初に各間隔の弓と矢をプリセットし、次に間隔内のすべての風船を横断し、風船を爆発させた弓と矢を間隔の右端の境界に移動することを検討し、それが次の間隔の風船と重なるかどうかを確認します。 、弓と矢を節約できます。

class Solution {
    
    
    public int findMinArrowShots(int[][] points) {
    
    
        // 最多需要的弓箭数
        int ans = points.length;
        // 将区间按照结尾的大小进行增序排序
        List<int[]> pointList = Arrays.stream(points).sorted(Comparator.comparingInt(point -> point[1])).collect(Collectors.toList());
        int preIndex = 0;
        for (int i = 1; i < pointList.size(); i++) {
    
    
            // 比较后一个区间是否和前一个区间重叠,重叠则可节约一支弓箭
            if (pointList.get(i)[0] <= pointList.get(preIndex)[1]) {
    
    
                ans--;
                continue;
            }
            preIndex = i;
        }
        return ans;
    }
}

複雑さの分析

  • 時間計算量: O(nlogn)、n は配列点の長さです。ソートの時間計算量は O(nlogn)、すべてのバルーンを走査して答えを計算する時間計算量は O(n) で、前者より漸近的に小さいため、無視できます。
  • スペース複雑度: O(logn)、ソートに必要なスタックスペースです。

4.1.3. 文字間隔を分割する

4.1.3.1、トピックの説明

763. アルファベット間隔の分割

文字列はS小文字で構成されます。この文字列をできるだけ多くのフラグメントに分割する必要があり、同じ文字が現れるのは最大 1 つのフラグメントです。各文字列フラグメントの長さを表すリストを返します。

4.1.3.2. 入出力の例

例 1:
入力: S = "ababcbacadefegdehijhklij"
出力: [9, 7, 8]
説明:
除算結果は、"ababcbaca"、"defegde"、"hijhklij" です。
各文字は最大でも 1 つのフラグメントに表示されます。
「ababcbacadefegde」、「hijhklij」のような分割は、分割数が少ないため間違っています。

4.1.3.3、問題の解決策

同じ文字は同じフラグメント内にのみ出現できるため、同じ文字が最初に出現する下付き文字の位置と最後に出現する下付き文字の位置は、同じフラグメント内に出現する必要があります。したがって、文字列を走査して、各文字が最後に出現する添え字の位置を取得する必要があります。


各文字が最後に出現する添え字の位置を取得した後、貪欲な方法を使用して文字列をできるだけ多くのセグメントに分割できます。具体的な方法は、文字列を左から右に走査して、現在のセグメント内のすべての文字が最後に出現する最大値を取得することです。添え字の終わりにアクセスすると、現在のセグメントへのアクセスが終了し、次の文字の検索が行われます。セグメントは文字列が走査されるまで続きます。

class Solution {
    
    
    public List<Integer> partitionLabels(String s) {
    
    
        // 统计每个字母最后一次出现的下标位置
        int[] last = new int[26];
        for (int i = 0; i < s.length(); i++) {
    
    
            last[s.charAt(i) - 'a'] = i;
        }
        List<Integer> partition  = new ArrayList<>();
        int start = 0, end = 0;
        // 从左到右遍历字符串,获取当前片段所有字母最后一次出现的最大值end
        for (int i = 0; i < s.length(); i++) {
    
    
            end = Math.max(last[s.charAt(i) - 'a'], end);
            // 当访问到下标end时,当前片段访问结束
            if (i == end) {
    
    
                partition .add(end - start + 1);
                start = end + 1;
            }
        }
        return partition ;
    }
}

複雑さの分析

  • 時間計算量: O(n)、n は文字列の長さです。文字列を 2 回走査し、最初の走査で各文字の最後の添字位置を記録し、2 回目の走査で文字列を分割する必要があります。
  • 空間計算量: O(∣Σ∣)ここで、Σ は文字列内の文字のセットです。この質問では、文字列には小文字のみが含まれているため、∣Σ∣=26 となります。

4.1.4. 株を売買するのに最適な時期 II

4.1.4.1、トピックの説明

122. 株式を売買するのに最適な時期 II では、

整数配列 価格 が得られます。ここで、 は、prices[i]その日の特定の株式の価格を表しますi

毎日、株を買うか売るかを決定できます。一度に保有できる株式は1株までです。最初に購入して、同日に販売することもできます。

あなたが得ることができる最大の利益を返します。

4.1.4.2、入力と出力の例

例 1:
入力:価格 = [7, 1, 5, 3, 6, 4]
出力: 7
説明: 2 日目に購入 (株価 = 1)、3 日目に購入 (株価 = 5) 売却すると、これ取引により利益が得られる = 5 - 1 = 4。
そして、4日目に買って(株価=3)、5日目に売る(株価=6)とすると、この取引では利益=6-3=3が得られます。
利益の合計は 4 + 3 = 7 です。

例 2:
入力:価格 = [1, 2, 3, 4, 5]
出力: 4
説明: 1 日目に購入 (株価 = 1)、5 日目に購入 (株価 = 5) 売り、このトランザクションは利益 = 5 - 1 = 4。
合計利益は4です。

例 3:
入力:価格 = [7, 6, 4, 3, 1]
出力: 0
説明:この場合、取引ではプラスの利益が得られないため、取引に参加しないことで最大の利益を得ることができ、最大利益は0です。

4.1.4.3、問題の解決策

株の購入には制限がないので、欲張って毎日取引して利益が0より大きい範囲を選ぶこともできます。 ただし、例の説明とは異なりますので注意が必要です。貪欲アルゴリズムは最大利益を計算するためにのみ使用され、計算プロセスは実際の取引プロセスではありません。

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        int ans = 0;
        for (int i = 1; i < prices.length; i++) {
    
    
            // 统计利润大于0的区间
            ans += Math.max(0, prices[i] - prices[i - 1]);
        }
        return ans;
    }
}

複雑さの分析

  • 時間計算量: O(n)、n は配列の長さです。配列を 1 回繰り返すだけで済みます。
  • 空間複雑度: O(1)。複数の変数を格納するための一定の空間のみが必要です。

4.2. 上級の難易度

4.2.1. 高さに基づいてキューを再構築する

4.2.1.1、トピックの説明

406. 高さに基づいてキューを再構成する

人々のグループがランダムな順序でキューに並んでおり、配列はpeopleキュー内の何人かの人々の属性を表しているとします (順序どおりである必要はありません)。それぞれ、people[i] = [hi, ki]i 番目の人の身長は でありhiちょうどki前にそれ以上の身長のhi人がいることを意味します。入力配列で表されるキューを

再構築して返してください。people返されるキューは、配列としてフォーマットする必要がありますqueue。ここで、はqueue[j] = [hj, kj]キュー内の人jの属性(queue[0]キューの先頭にいる人) です。

4.2.1.2. 入出力例

例 1:
入力: people = [ [7, 0], [4, 4], [7, 1], [5, 0], [6, 1], [5, 2] ] 出力: [ [
5 , 0]、[7, 0]、[5, 2]、[6, 1]、[4, 4]、[7, 1] ] 説明: 0 番の人の身長は 5 で、これより背の高い人はいません。あるいは彼の前に同じ人が並んでいた
1 番の人の身長は 7 で、彼の前にはそれより背が高い、または同じ人はいません。2 番の人の身長は 5 で、その人の前にそれより高いか同じ身長の 2 人、つまり 0 番と 1 番の人がいます。3 番の人の身長は 6 で、その人の前に自分より背が高いか同じ身長の人、つまり 1 番の人が 1 人います。4 番の人の身長は 4 で、その人の前にはそれより高いか同じ身長の人、つまり 0、1、2、3 の 4 人がいます。5番の人の身長は7で、その人の前に自分より背が高いか同じ身長の人、つまり1番が1人います。したがって、[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] は再構築されたキューです。






例 2:
入力: people = [ [6, 0], [5, 0], [4, 0], [3, 2], [2, 2], [1, 4] ] 出力: [ [
4 , [0]、[5, 0]、[2, 2]、[3, 2]、[1, 4]、[6, 0]]

4.2.1.3、問題の解決策

まず身長の小さい方から大きい方、前の人の背の高い方、または同じ人数の小さい方から大きい方に基づいて並べ替え、次に並べ替えられたキューを横断し、各要素の前に対応するスペースを残し、質問文の条件を満たします。元のキューを復元するのに非常に便利です。

class Solution {
    
    
    public int[][] reconstructQueue(int[][] people) {
    
    
        // 按照身高从小到大,前面身高更高或者相同的人数从少到多进行排序
        Arrays.sort(people, new Comparator<int[]>() {
    
    
            @Override
            public int compare(int[] people0, int[] people1) {
    
    
               if (people0[0] != people1[0]) {
    
    
                   return people0[0] - people1[0];
               } else {
    
    
                   return people0[1] - people1[1];
               }
            }
        });
        // 重新构造的队列
        int[][] ans = new int[people.length][];
        
        // 遍历重新排序后的队列
        for (int[] person : people) {
    
    
            // 需重新入队人员的前面身高更高或者相同的人数,为0时则入队
            int index = person[1];
            // 遍历重新构造的队列(未完全完成)
            for (int j = 0; j < ans.length; j++) {
    
    
                if (index == 0) {
    
    
                    if (ans[j] == null) {
    
    
                        ans[j] = person;
                        break;
                    }
                    continue;
                }
                // 确保需入队人员前面身高更高或者相同的人数要求
                if (ans[j] == null || ans[j][0] >= person[0]) {
    
    
                    index--;
                }
            }
        }
        return ans;
    }
}

複雑さの分析

  • 時間計算量: O(n 2 )、n は配列 people の長さです。並べ替えには O(nlogn) 時間が必要で、その後、各人を反復処理してキューに入れるにはO(n 2 ) 時間がかかります。前者は後者より漸近的に小さいため、合計の時間計算量は O(n 2 ) になります。
  • 空間複雑度: O(logn)

4.2.2. 非減少シーケンス

4.2.2.1、トピックの説明

665. 非減少数列 長さ の整数配列が

与えられた場合、最大の要素を変更するだけで配列が非減少数列になれるかどうかを判断してください。これは、非減少配列を定義する方法です。配列内の任意のものについては、常に満たされますnnums1

i (0 <= i <= n-2)nums[i] <= nums[i + 1]

4.2.2.2. 入出力例

例 1:
入力: nums = [4, 2, 3]
出力: true
説明:最初の 4 を 1 に変更することで、非減少シーケンスにできます。

例 2:
入力: nums = [4, 2, 1]
出力: false
説明: 1 つの要素だけを変更して非減少シーケンスにすることはできません。

4.2.2.3、問題の解決策

変更される要素が最大 1 つであることを保証するという前提の下で、配列が非減少シーケンスに変更される場合は、配列を走査し、非減少条件に遭遇したときに変更してカウントし、2 番目の変更時に失敗するだけで済みます。が発生します。


トラバーサル中に、次の要素が前の要素より小さい場合、それが配列の先頭または末尾であれば、配列の最初の要素を設定していると見なすことができるため、配列を変更する必要はなく、カウントするだけです。最小値に設定するか、配列を設定するか、最終的には最大値に設定されたと思いますが、後で使用することはないので、実際に変更する必要はありません。


配列の途中にある場合、このときの処理方法は実際には 2 つあります。1 つは前の値と同じになるように値を変更する方法、もう 1 つは前の値を前の値と同じになるように変更する方法です。 value (ここでは、前の value を変更した後、その非減少条件に影響を与えないことを確認する必要があります)、この値は後続の値と比較する必要があるため、最初に 2 番目の方法を使用する必要があります。値ができるだけ小さいことを確認してください。

class Solution {
    
    
    public boolean checkPossibility(int[] nums) {
    
    
        for (int i = 1, count = 0; i < nums.length; i++) {
    
    
            if (nums[i] < nums[i - 1]) {
    
    
                if (i != 1 && i != nums.length - 1 && nums[i] < nums[i - 2]) {
    
    
                    nums[i] = nums[i - 1];
                }
                if (++count > 1) {
    
    
                    return false;
                }
            }
        }
        return true;
    }
}

複雑さの分析

  • 時間計算量: O(n)、ここで n は配列 nums の長さです。
  • 空間複雑度: O(1)

おすすめ

転載: blog.csdn.net/rockvine/article/details/125472114