Advanced Advanced Class 1 - Supplement (プロセスを最適化するための自明なソリューションの使用、レコード構造による可能性分割の境界条件の発見、動的計画法テーブル充填のための傾き最適化手法、上部中央値構造)

目次

【事例1】

[タイトル説明]

【アイデアの分析】

【コード】

【事例2】

[タイトル説明] 

[タイトル説明]

【コード】

 【事例3】

[タイトル説明]

【アイデアの分析】

【コード】

【事例4】

[タイトル説明]

【アイデア分析1】

【アイデア分析2】

【アイデア分析3】

[コード実装は最適解のコードのみを実装します。対数マイザーは方法 1 を使用します]


【事例1】

[タイトル説明]

【アイデアの分析】

まず、配列を走査して配列全体の最小値と最大値を取得し、閉じた区間[最小値、最大値]をn+1個の小さな区間に分割すると、配列全体のn個の数値が分配されますこれら n+1 個の小さな間隔では、特定の間隔に複数の数値が含まれる可能性がありますが、これらの数値間の隣接する差は間隔の長さよりも小さくなければなりません。また、n+1 個の小さな間隔があるため、いくつかの間隔が存在する必要がありますこれらの空の間隔の左右にある隣接する数字の差は、間隔の長さより大きくなければなりません。(つまり、これらの n+1 個の小さな区間は、空の区間があるため、自明な解 (区間の長さ) を与えます。したがって、自明な解よりも小さくなければならない解を考慮する必要はありません。したがって、次のことを解く必要はありません。同じ間隔のソリューション内の隣接する数値の場合)。各区間の最小値と最大値を記録し、それをさまざまな区間で解きます。

【コード】

/**
 * @ProjectName: study3
 * @FileName: Ex1
 * @author:HWJ
 * @Data: 2023/9/16 15:57
 * 利用平凡解优化的技巧
 */
public class Ex1 {
    public static void main(String[] args) {

    }

    public static int getMaxDeviation(int[] arr){
        if (arr.length < 2) {
            return -1; // 只有一个数字的数组,无法得到两个相邻数的差
        }
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        int len = arr.length;
        for (int i = 0; i < len; i++) {
            max = Math.max(max, arr[i]);
            min = Math.min(min, arr[i]);
        }
        if (max == min){ // 满足此条件表示整个数组为常数数组。
            return 0;
        }
        boolean[] hasNum = new boolean[len + 1];
        int[] maxs = new int[len + 1];
        int[] mins = new int[len + 1];
        int index = 0;
        for (int i = 0; i < len; i++) {
            index = getIndex(len + 1, arr[i], min, max);
            maxs[index] = hasNum[index] ? Math.max(maxs[index], arr[i]) : arr[i];
            mins[index] = hasNum[index] ? Math.min(mins[index], arr[i]) : arr[i];
            hasNum[index] = true;
        }
        int lastMax = maxs[0];
        int res = Integer.MIN_VALUE;
        for (int i = 1; i < len + 1; i++) {
            if(hasNum[i]){
                res = Math.max(mins[i] - lastMax, res);
                lastMax = maxs[i];
            }
        }
        return res;
    }

    public static int getIndex(int len, int num, int min, int max){
        return (int) (num - min) * len / (max - min);
    }
}

【事例2】

[タイトル説明] 

 n 個の数値 a1,...an が与えられた場合、できるだけ多くの部分で XOR の合計が 0 に等しくなるように、この配列を複数の部分に分割する方法を尋ねます。最大数はいくらなのか尋ねてください。

[タイトル説明]

i 位置の任意の数値については、可能性は 2 つだけです。(1) 最適な除算の場合、k 位置から i 位置までの XOR 和を 0 に等しくすることができます。(2) 以下から XOR 和を作成できます。最適除算の場合、k 位置から i 位置までが 0 に等しい 特定の部分の XOR 和を 0 に等しくすることはできません。したがって、最適な分割の下で i 位置が k 位置から i 位置までの XOR 合計を 0 に等しくできるかどうか、最も近い k 位置をどのように見つけるかを解決する必要があります。トラバーサル中に 0-i の位置からの XOR 合計を記録できます。XOR 合計は m で、m に到達した最後の位置は k-1 です。このレコード構造を利用すると、XOR和が0になる部分を見つけ出し、非繰り返し条件での最適な分割状況を求めることができます。

【コード】

import java.util.HashMap;

/**
 * @ProjectName: study3
 * @FileName: Ex2
 * @author:HWJ
 * @Data: 2023/9/16 16:31
 */
public class Ex2 {
    public static void main(String[] args) {
        int[] arr= {3,2,1,4,0,4,0,3,2,1};
        System.out.println(getBestDivision(arr));
    }

    public static int getBestDivision(int[] arr){
        int[] division = new int[arr.length];
        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, -1); // 初始化记录表
        int xor = 0;
        for (int i = 0; i < arr.length; i++) {
            xor ^= arr[i];
            if (map.containsKey(xor)){
                int pre = map.get(xor);
                division[i] = pre == -1 ? 1 : Math.max(division[pre] + 1, division[i - 1]);
            }
            map.put(xor, i);
        }
        return division[arr.length - 1];
    }
}

 【事例3】

[タイトル説明]

【アイデアの分析】

この質問は、a の額面を完成させるために普通の硬貨を使用する方法が x 通りあり、ma の額面を完成させるために記念硬貨を使用する方法が y 通りあると見ることができます。すると普通硬貨と記念硬貨を使ってmの額面を完成させる方法はx*yとなります。したがって、2 つの動的計画法を使用して、普通硬貨が額面 0...m を完成する方法の数と、記念硬貨が額面 0...m を完成する方法の数を取得します。

通常のコインの動的プログラミングは、傾きの最適化を実行できます。

1 0 0 1 0
1
1

上記の表は、通常の通貨動的プログラミングで入力する必要がある表として使用できます。上記の表の dp[i, j] は、0...i 型を使用して j 額面を完成させる方法の数を表します。3 つの通貨、つまり 3、2、1 があり、完了する必要がある額面金額が 4 であるとします。最初の列は、定義により、i 通貨を使用して額面 0 を完成させる方法の数として表されます。つまり、コインを使用せずにすべて 1 になります。最初の行は、通貨 3 を使用して額面 j を完成する方法の数を表し、次の行は、2 つの通貨 2 と 3 を使用して額面 j を完成する方法の数を表します。この行の任意の j の位置について、dp[i-1][j] に依存します。 dp[i-1][j - 3] dp[i-1][j - 6]....j - 3 の場合、dp[i- に依存します。 1][ j - 3] dp[i-1][j - 6]....つまり、任意の j 位置に対して dp[i][j] = dp[i-1][j として最適化できます。 ] + dp[i][j - 3] 。

【コード】

/**
 * @ProjectName: study3
 * @FileName: Ex3
 * @author:HWJ
 * @Data: 2023/9/16 17:09
 */
public class Ex3 {
    public static void main(String[] args) {
        int[] arr1 = {2,3,4};
        int[] arr2 = {2,3,1};
        System.out.println(dpWays(arr1, arr2, 8));
    }

    public static int dpWays(int[] arr1, int[] arr2, int m){
        int[][] dp1 = new int[arr1.length][m + 1];
        int[][] dp2 = new int[arr2.length][m + 1];
        for (int i = 0; i < arr1.length; i++) {
            dp1[i][0] = 1;
        }
        for (int i = arr1[0]; i < m + 1; i += arr1[0]) {
            dp1[0][i] = 1;
        }
        for (int i = 1; i < arr1.length; i++) {
            for (int j = 1; j < m + 1; j++) {
                dp1[i][j] = dp1[i - 1][j] + ((j - arr1[i]) >= 0 ? dp1[i][j - arr1[i]] : 0);
            }
        }

        for (int i = 0; i < arr2.length; i++) {
            dp2[i][0] = 1;
        }
        dp2[0][arr2[0]] = 1;
        for (int i = 1; i < arr2.length; i++) {
            for (int j = 1; j < m + 1; j++) {
                dp2[i][j] = dp2[i - 1][j] + ((j - arr2[i]) >= 0 ? dp2[i - 1][j - arr2[i]] : 0);
            }
        }
        int ans = 0;
        for (int i = 0; i < m + 1; i++) {
            ans += dp1[arr1.length - 1][i] * dp2[arr2.length - 1][m - i];
        }
        return ans;
    }
}

【事例4】

[タイトル説明]

【アイデア分析1】

外部ソートに 2 つのポインターを使用すると、時間計算量は O(K) になります。最も栄養価の低い溶液。

【アイデア分析2】

これらは 2 つのソートされた配列であるため、二分探索が可能である必要があります。

k 番目の数字が A にある場合と、k 番目の数字が B にある場合の 2 つの状況があります。

A 配列で二分検索を実行して数値を見つけるたびに、この数値を使用して、B 配列で二分検索を実行します。このようにして、この数字は A では a + 1、B では b + 1 にランクされることがわかります。そして、合計で a + b + 1 にランク付けされます。a + b + 1 と k の関係を通じて、決定することができます次の二分探索の戦略。最終的に A で見つかった場合はその番号を返し、そうでない場合は B でこのような検索処理を実行します。時間計算量は O(logM*logN) です。

【アイデア分析3】

上部中央値の定義は、同じ長さの 2 つの配列の上部中央値であるため、合計の長さは偶数である必要があり、合計の長さが 4 (abcd) であると仮定します。真ん中に b と c という 2 つの数字がありますが、b が上部中央値、c が下部中央値であると考えます。

2 つの配列の上位中央値を返す関数を定義します。2 つの配列は同じ長さである必要があります。

次に、この関数の実装を分析してみましょう。ここでは奇数の場合の分析方法を紹介しますが、偶数の場合はより簡単で自分でも実装できます。

ある b c d e
1 2 3 4 5

毎回、2 つの配列の中央の数値 (a.lenth - 1)/2 を見つけます。c ==3 の場合、その値を返します。それらは 2 つの配列の中央値である必要があります。それ以外の場合は、次のように設定することもできます。 c >3 の場合、どの数値が上位中央値になる可能性があるかを考えてみましょう。青でマークされた数値が上位中央値になる可能性があります。しかし、このように分割した場合、2 つの配列の長さが等しい必要があるため、再帰を実行する方法がありません。そのため、3 が上部中央値であるかどうかを個別に検証し、ab 4 5 を使用して除外後に再帰を実行します。再帰的な上部中央値は全体の中央値です。

さて、この関数ができたので、全体的な可能性を分割します。

長い配列の長さは n で、短い配列の長さは m です。

(1)k <=m。

上位中央値を見つけるために、long 配列の最初の k 要素と short 配列の最初の k 要素を直接選択し、戻り値は k 番目の数値になります。

(2) m < k <= n

m < k <= n の場合、k 番目の桁として使用してはならない特定の数値を確実に除外し、削除することができます。残りの 2 つの配列の長さは同じではない可能性があります (長い配列にはもう 1 つあります)。ただし、個々の数値を個別に検証して 2 つの配列の長さを等しくしてから、残りの 2 つの配列を使用して上部の中央値を見つけることができます。

(3)k > n

k>n の場合、k 番目の桁として使用してはならない特定の数値をフィルタリングして削除できる必要があります。残りの 2 つの配列は同じ長さである必要があり、これらの残りの 2 つの配列を使用して解決します。中央値の場合。ただし、私たちの機能は上位中央値を見つけることなので、同じ長さのこれら 2 つの配列の最初の要素をテストしてから、残りの 2 つの配列の上位中央値を見つけます。

取り出される各数値の再帰の長さは m を超えないため、時間計算量は O(logM) になります。

[コード実装は最適解のコードのみを実装します。対数マイザーは方法 1 を使用します]

package AdvancedPromotion2;

import java.util.Arrays;
import java.util.Random;

/**
 * @ProjectName: study3
 * @FileName: Ex4
 * @author:HWJ
 * @Data: 2023/9/17 15:23
 */
public class Ex4 {
    public static void main(String[] args) {
        Comparator();
    }

    public static int getKDigitNum(int[] arr1, int[] arr2, int k){ // 这里保证k合法, 即在调用函数之前,验证k的合法性。
        int n = Math.max(arr1.length, arr2.length);
        int m = Math.min(arr1.length, arr2.length);
        int[] longs = n == arr1.length ? arr1 : arr2;
        int[] shorts = n == arr1.length ? arr2 : arr1;
        if (k <= m){
            return upperMedian(shorts, longs, 0, k - 1, 0, k - 1);
        } else if (k <= n) {
            int l = k - m;
            if (longs[l - 1] > shorts[m - 1]){
                return longs[l - 1];
            }else {
                return upperMedian(shorts, longs, 0, m - 1, l, k - 1);
            }
        } else {
           int s = k - n;
           int l = k - m;
           if (longs[l - 1] >= shorts[m - 1]){
               return longs[l - 1];
           } else if (shorts[s - 1] >= longs[n - 1]) {
               return shorts[s - 1];
           }else {
               return upperMedian(shorts, longs, s, m - 1, l , n - 1);
           }
        }
    }

    // 求上中位数的函数
    public static int upperMedian(int[] arr1, int[] arr2, int s1, int e1, int s2, int e2) {
        if (s1 == e1) { // 当两个数组只有一个数时,进入baseCase
            return Math.min(arr1[s1], arr2[s2]);
        }
        int mid1 = (e1 - s1) / 2 + s1;
        int mid2 = (e2 - s2) / 2 + s2;
        if (arr1[mid1] == arr2[mid2]) {
            return arr1[mid1];
        } else if (arr1[mid1] > arr2[mid2]) {
            if ((e1 - s1) % 2 != 0) { // 数组长度为偶数情况下
                return upperMedian(arr1, arr2, s1, mid1, mid2 + 1, e2);
            } else { // 数组长度为奇数情况下
                if (arr2[mid2] >= arr1[mid1 - 1]) {
                    return arr2[mid2];
                } else {
                    return upperMedian(arr1, arr2, s1, mid1 - 1, mid2 + 1, e2);
                }
            }

        } else {
            if ((e1 - s1) % 2 != 0) { // 数组长度为偶数情况下
                return upperMedian(arr1, arr2, mid1 + 1, e1, s2, mid2);
            } else { // 数组长度为奇数情况下
                if (arr1[mid1] >= arr2[mid2 - 1]) {
                    return arr1[mid1];
                } else {
                    return upperMedian(arr1, arr2, mid1 + 1, e1, s2, mid2 - 1);
                }
            }
        }
    }

    public static int compare(int[] arr1, int[] arr2, int k){
        int p1 = 0;
        int p2 = 0;
        int ans = 0;
        while (p1 + p2 < k){
            if (p1 != arr1.length && p2 != arr2.length){
                if (arr1[p1] < arr2[p2]){
                    ans = arr1[p1++];

                }else {
                    ans = arr2[p2++];
                }
            }else {
                if (p1 == arr1.length){
                    ans = arr2[p2++];
                }else {
                    ans = arr1[p1++];
                }
            }

        }
        return ans;
    }

    public static void Comparator(){
        Random random = new Random();
        int times = 50000;
        int size = 1000;
        int max = 10000;
        for (int i = 0; i < times; i++) {
            int[] arr1 = new int[random.nextInt(size) + 100];
            int[] arr2 = new int[random.nextInt(size) + 100];
            for (int j = 0; j < arr1.length; j++) {
                arr1[j] = random.nextInt(max);
            }
            for (int j = 0; j < arr2.length; j++) {
                arr2[j] = random.nextInt(max);
            }
            int k = random.nextInt(arr1.length + arr2.length) + 1;
            Arrays.sort(arr1);
            Arrays.sort(arr2);
            int ans1 = getKDigitNum(arr1, arr2, k);
            int ans2 = compare(arr1, arr2, k);
            if (ans1 != ans2){
                System.out.println("失败!!!");
                System.out.println(ans1 + " " + ans2 + "  " + k);
                System.out.println(Arrays.toString(arr1));
                System.out.println(" ------------------ ");
                System.out.println(Arrays.toString(arr2));
                break;
            }
        }
        System.out.println("成功!!!");
    }
}

おすすめ

転載: blog.csdn.net/weixin_73936404/article/details/132897175