某網際網路大廠四道演算法題引發的思考

劈瓜者刘华强發表於2024-03-21

試題一

輸入包含多組測試資料,第一行輸入測試陣列的數量n,其中n小於等於10^5。接下來的再輸入2*n個數,每個數為小於等於10^9的非0正數。組成一個長度為2n的陣列。接下來每次在陣列中取兩個數,組成一個座標並且將其繪製在平面座標系上面(一共可以取n次)。再用一個矩形去圍住這些點,選擇一種取數方式,要求最後得到的矩形的面積最小。輸出最小的面積即可。
輸入樣式:
2
1 2 3 5

輸出樣式:
2

演算法思路

每一次測試的資料量最多為10^5,因此不適合用蠻力法(蠻力法通常至少包含兩個迴圈)。進一步分析,題目只要求輸出最小矩形的面積,由此想到對問題轉化。要使面積最小,實際上就是矩形長乘以寬要最小。因此可以考慮貪心策略。使長和寬儘量都小。而矩形的長(寬)是由所有的座標中橫(縱)座標最大值減去最小值的查決定的,為了使這個值最小,可以先對陣列從小到大排序,排完序之後的陣列a的長度為2n。則此時最小的面積應當為:(a[2n-1]-a[n])*(a[n-1]-a[0])。值得注意的是a[2n-1]-a[n]或者a[n-1]-a[0]的值可能為0,此時應當進行特殊處理,將為0的值替換為1(因為矩形長和寬最短的情況下應該為1)【當時上級的時候沒想到這種情況,導致後面提交透過率一直是87%左右😅😅】

演算法程式碼

package basic_language.byte_dance;

import java.util.Arrays;
import java.util.Scanner;

/**
 * 第一行輸入n
 * 第二行輸入2n個資料
 * 對這2n個資料進行劃分,分成n個座標對,用一個矩形在xy座標系下圍住這些點,求矩形最小的面積
 */
public class Test1 {

    public static void main(String[] args) {
        int n;
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            // 接收使用者的輸入
            n = scanner.nextInt();
            int[] array = new int[2 * n];
            for (int i = 0; i < 2 * n; i++) {
                array[i] = scanner.nextInt();
            }
            // 只有一個點直接返回1
            if(n==1) {System.out.println(1);continue;}
            Arrays.sort(array);
            if(array[2*n-1]-array[n]==0){
                if(array[n-1]-array[0] ==0){
                    System.out.println(1);
                }else{
                    System.out.println(array[n-1]-array[0]);
                }
            }else{
                long result= (array[2*n-1]-array[n])*(array[n-1]-array[0]);
                System.out.println(result==0L? array[2*n-1]-array[n] : result);
            }

        }
    }
}

點評

第一題實際上玩了一個智力遊戲,腦子靈活一點很容易想到思路。注意對應的細節即可

試題二

接收如下的輸入,第一行輸入n,n<=10^5。第二行輸入n個數,每一個數的範圍0<=n<=10^9。在裡面找到連續的子陣列,使其成為3和5的倍數但不是4的倍數,輸出滿足該條件的所有子陣列的數量。
輸入樣例:
3
1 14 15

輸出樣例:
3

說明:三個子陣列分別是[1,14],[15],[1,14,15]

演算法思路

首先排除蠻力法,對於較小的n值(小於等於1000),用蠻力法還是可以應對的。但是當n的值比較大的時候,使用蠻力法將浪費大量的時間(後面會有對比)。因此我們需要轉變思路,從蠻力法開始思考。蠻力法之所以比較浪費時間,是因為需要反覆計算某個區間的元素之和。例如計算a[1]+a[2]+a[3]+a[4]同時也包含子陣列a[2]+a[3]+a[4]的計算,因此重複的計算太多了。這是演算法大忌。因此我們需要減少重複計算的次數,減少程式碼執行時間。一種典型的思路就是以空間換時間。

  1. 申請長度為n的陣列sums,其中sums[i]=a[0]+a[1]+...+a[i]。之所以這樣設計,是因為假設後面需要求解區間[i+1,j]的元素之和,只需要使用sums[j]-sums[i]即可(如果不好理解隨便舉個例子即可)
  2. 但是1中的方式還是避免不了要重複遍歷sums陣列,因為為了判斷從下標0開始的子陣列中元素之和滿足輸出要求的需要一直遍歷到sums陣列末尾。同理為了判斷從下標1開始的子陣列中元素之和滿足輸出要求的也需要一直遍歷到sums陣列末尾,實際上程式碼的複雜度還是O(N^2),還是不可以接受。
  3. 正當我苦思冥想的時候,突然想到模運算的相關運算規則,如下:

a%x - b%x ==0 <=> (a-b)%x==0
上面的式子證明比較簡單,這裡就不證明了。【當時在考試的時候,我還在為我的急中生智而竊喜,腦子怎麼這麼靈活,是吧。後來考試結束,我在網上一搜,原來這是一種常用的技巧,只不過是我之前不知道罷了。我真的裂開!😋😋】
現在就是神來之筆了,既然有上面的等式。那我們可以按照如下的方式充分運用上面的等式:

  1. 申請長度為15的陣列temp,其中temp[i]表示值為i的元素的個數。初始化temp陣列的時候,temp[sums[i] % 15]++。即遍歷sums陣列,將陣列中的每一個元素模15之後作為下標更新temp陣列,這樣temp[i]中記錄的都是sums陣列中模15的值都是i的元素的個數。再回到那個等式:

a%x - b%x ==0 <=> (a-b)%x==0
猜猜我想到了什麼,假設b,c,d是整個sums陣列中模15等於i的數,那麼此時temp[i]應該等於3。而此時滿足題目輸出要求的子陣列的和應當為:
c-b, d-b, d-c, 相應在這一步應當更新最終返回結果加三。這不就是在a,b,c三個數之中任取兩個數相減,並且保證相減的時候是大減小。這不就是排列組合嗎?老天爺!因此實際上我們根本不用關係子陣列的和是多少。

  1. 只需要知道temp[i]的值,從而更新最後結果加上\(C_{temp[i]}^2\),也就是temp[i]*temp[i-1]/2。遍歷一遍temp,這樣我們就找到能夠整除15的子陣列的數量,記為result。

上面用到了高中排列組合知識,也就是大學機率論裡面的古典概型
接下來需要解決的問題就是處理不能被4整除,此時肯定不能用上面的那個等式。此時需要腦子靈活一點,逆向思維
透過temp求得滿足題目輸出條件的result之後,接下來肯定需要減去某個數以排除能整除4的數,但是應該怎麼排除呢。實際上很簡單,即在能整除15的字陣列中排除那些能夠被4整除的子陣列。

  1. 按照4和5中的思路,再申請一個長度為60的陣列temp2,temp2[i]中記錄的都是sums陣列中模60的值都是i的元素的個數,這樣我們就找到能夠整除60的子陣列的數量,記為result1。【為什麼是60呢?為什麼不是4呢。如果是4,此時temp2[i]中記錄的都是sums陣列中模4的值都是i的元素的個數,而在這些數中有一些是不能被15整除的,因此我們需要滿足基本條件被15整除,透過下面的韋恩圖可以更好的理解】
  2. 最終返回的結果是result-result1。即在能整除15的字陣列中排除那些能夠被4整除的子陣列

演算法程式碼

package basic_language.byte_dance;

import java.util.Random;
import java.util.Scanner;

/**
 * 第一行輸入n,n<=10^5
 * 第二行輸入n個數,每一個數的範圍0<=n<=10^9
 * 在裡面找到連續的子陣列,使其成為3和5的倍數但不是4的倍數,輸出滿足該條件的所有子陣列的數量
 */
public class Test2 {
    public static void main(String[] args) {
        int n;
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            n = scanner.nextInt();
            // 接收輸入
            int[] array = new int[n];
            for (int i = 0; i < n; i++) {
                array[i] = scanner.nextInt();
            }
            // 使用額外一個陣列記錄元素之和,陣列名稱為sums,其中sums[i]的含義為:a[0]+a[1]+...+a[i]
            long[] sums = new long[n];
            sums[0] = array[0];
            for (int i = 1; i < n; i++) {
                sums[i] = sums[i - 1] + array[i];
            }
            // 處理子數字長度為1的情況,正如步驟1中提到的那樣:
            // 求解區間[i+1,j]的元素之和,只需要使用sums[j]-sums[i]即可
            // 求解以0為開頭的區間需要單獨考慮
            long result = 0;
            for (int i = 0; i < n; i++) {
                if (sums[i] % 15 == 0 && sums[i] % 4 != 0) {
                    result++;
                }
            }
            // 申請長度為15的陣列temp,temp[i]的含義是值為i的元素的個數
            // 將sums陣列中的元素模15後加入temp當中
            int[] temp = new int[15];
            for (int i = 0; i < n; i++) {
                temp[(int) (sums[i] % 15)]++;
            }

            // 遍歷temp,如果temp[i] !=0 則臨時結果result加上(temp[i]-1)*temp[i]/2
            for (int i = 0; i < 15; i++) {
                result += temp[i] * (temp[i] - 1) / 2;// 這裡需要注意細節,很重要,先算乘法再算處罰。否則按照Java語言規則,如果a不能整除b有餘數的話,Java會自動向下取整。造成資料丟失。實際上就是一個小細節需要注意
            }

            // 再申請長度為60的陣列temp2,temp2[i]的含義是值為i的元素的個數
            int[] temp2 = new int[60];
            for (int i = 0; i < n; i++) {
                temp2[(int) (sums[i] % 60)]++;
            }
            // 將sums陣列中的元素取模60加入到temp2中
            // 遍歷temp2,如果temp2[i] !=0 則臨時結果result減去(temp2[i]-1)*temp[i]/2
            for (int i = 0; i < 60; i++) {
                result -= temp2[i] * (temp2[i] - 1) / 2;
            }
            System.out.println(result);
        }
    }
}

進一步思考:和蠻力法比較

在上面程式碼的基礎上,新增如下兩個方法:

/**
     * 隨機生成長度為length的陣列
     *
     * @param length
     * @return
     */
    private static int[] generate(int length) {
        int[] array = new int[length];
        Random random = new Random();
        for (int i = 0; i < length; i++) {
            array[i] = random.nextInt(length);
        }
        return array;
    }

    /**
     * 暴力求解該問題,適用於array長度較小的情況
     * 每隔5秒列印依次程式執行情況
     *
     * @param array
     */
    private static void violentSolve(int[] array) {

        long currentTime = System.currentTimeMillis();
        int times = 1;
        long result = 0;
        for (int i = 0; i < array.length; i++) {

            for (int j = i; j < array.length; j++) {

                // 計算從[i,j]之和
                long sum = 0;
                for (int k = i; k <= j; k++) {
                    sum += array[k];
                }
                if (sum % 15 == 0 && sum % 4 != 0) {
                    result++;
                }
                long millis = System.currentTimeMillis();
                while (millis - currentTime > 5 * 1000 * times) {
                    System.out.print("蠻力法程式已經執行的時間,單位為秒:" + (millis - currentTime) / 1000.0);
                    System.out.println(";處理的區間為[ " + i + "," + j + " ]");
                    times++;
                }
                // 超過5分鐘程式還沒執行完,終止迴圈
                if (millis - currentTime >= 5 * 60 * 1000) {
                    System.out.print("使用蠻力法求解出來的值:" + result);
                    return;
                }
            }
        }
        System.out.print("使用蠻力法求解出來的值:" + result);
        System.out.println("。所用的時間為:" +(System.currentTimeMillis()-currentTime)/1000.0);
    }

上面的暴力求解方法將在程式執行過程中每隔5秒列印一次程式執行狀態,和按照以空間換取時間演算法相比,執行結果如下:

比較結果

相關文章