1. 程式人生 > 實用技巧 >LeetCode第 30 場雙週賽題解

LeetCode第 30 場雙週賽題解

5177. 轉變日期格式

思路:按照空格劃分出年月日,年已經是數字形式,不需要做處理。月是英文單詞的縮寫,需要對應到相應的數字。
對於日,只需要把結尾的英文序數詞去掉,只留下數字即可。 這裡要注意月和日可能是個位數,這種情況下要在前面補一個0.

class Solution {
string months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

public:
    string reformatDate(string date) {
        string day, month, year, res;
        int split = 0;                        //用雙指標劃分出字串
        while(isdigit(date[split])) {      
            day += date[split];
            ++split;
        }
        if(day.size() < 2) {                  //如果是個位數,在前面補一個0
            day = '0' + day;
        }
        while(date[split] != ' ') {           //指標繼續移動,劃分下一個字串(月)
            ++split;
        }
        ++split;
        while(date[split] != ' ') {
            month += date[split];
            ++split;
        }
        for(int i = 0; i < 12; ++i) {         //把月份的單詞對應到相應的數字上
            if(months[i] == month) {
                month = to_string(i + 1);
            }
        }
        if(month.size() < 2) {                //如果是個位數,在前面補一個0
            month = '0' + month;
        }
        ++split;
        year = date.substr(split);            //剩下的部分就是年
        //printf("%s-%s-%s", year.c_str(), month.c_str(), day.c_str());
        res = year + '-' + month + '-' + day;
        return res;
    }
};

這題劃分字串也可以不用雙指標,用stringstream可以直接以空格為分隔符劃分三個字串。
程式碼如下:

class Solution {
string months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

public:
    string reformatDate(string date) {
        stringstream ss(date);                  //用一個stringstream劃分date
        string day, month, year, temp, res;      //temp表示每一個劃分出的字串
        while(getline(ss, temp, ' ')) {         //使用空格作為分隔符
            if(isdigit(temp[0]) && !isdigit(temp.back())) {    //如果某個劃分出的字串temp第一個字元是數字而最後一個不是,則說明現在劃分出的字串是日
                while(!isdigit(temp.back())) {              //刪掉末尾的英文序數詞
                    temp.pop_back();
                }
                day = temp;                      
                if(day.size() < 2) {                       //如果是個位數,在前面補個0
                    day = '0' + day;
                }
            } else if(!isdigit(temp[0]) && !isdigit(temp.back())) {     //如果某個字串第一個字元和末尾都不是數字,說明現在劃分出的字串是月
                for(int i = 0; i < 12; ++i) {             
                    if(months[i] == temp) {
                        month = to_string(i + 1);             //把英文單詞轉化成對應的數字
                        break;
                    }
                }
                if(month.size() < 2) {                        //如果是個位數,在前面補個0
                    month = '0' + month;
                }
            } else if(isdigit(temp[0]) && isdigit(temp.back())) {      //首末都是數字,說明是年,不作處理
                year = temp;
            }
        }
        res = year + '-' + month + '-' + day;
        return res;
    }
};

這題用python做會方便很多:

class Solution:
    def reformatDate(self, date: str) -> str:
        day, month, year = date.split(' ')
        Months = {"Jan":"01", "Feb":"02", "Mar":"03", "Apr":"04", "May":"05", "Jun":"06", "Jul":"07", "Aug":"08", "Sep":"09", "Oct":"10", "Nov":"11", "Dec":"12"}
        month = Months[month]
        day = day[:2] if day[1].isdigit() else '0' + str(day[0])
        return '-'.join([year, month, day])

5445. 子陣列和排序後的區間和

這題題意是給定一個數組,求出他的所有子陣列的和的可能,再對子陣列的和排序組成新陣列,
對於新陣列中下標從left到right(left和right給定)的陣列再進算和並返回。

要計算所有子陣列和,最簡單的想法是列舉所有子陣列的起點和終點,再計算這一段的元素和。
同時列舉起點和終點,再在起點和終點裡計算和需要O(n^3)的複雜度。超時。

計運算元陣列和,常見的方法是用一個字首和陣列來優化時間。
額外用一個數組preSum表示每個元素的字首和,字首和就是從陣列第一個元素到當前元素的和。
比如preSum[3]就是nums[0] + nums[1] + nums[2] + nums[3]。
這樣做的好處就是,我們枚舉了起點i和終點j之後,要計算這一段子陣列的和不需要逐個相加,
可以直接用preSum[j] - preSum[i]得到子陣列和,這樣時間就壓縮到了O(n^2).

程式碼如下:

class Solution {
typedef __int64_t bignum;                        //這題資料很大,我們用__int64_t
bignum mod = 1e9 + 7;                            //返回結果要對1e9 + 7取餘
public:
    int rangeSum(vector<int>& nums, int n, int left, int right) {
        int size = nums.size();
        vector<bignum> preSum(size);
        preSum[0] = nums[0];
        for(int i = 1; i < size; ++i) {
            preSum[i] = preSum[i - 1] + nums[i];      //對所有元素計算字首和
        }
        vector<bignum> sums;                       //sums陣列儲存所有的子陣列和
        for(int i = 0; i < size; ++i) {
            sums.push_back(preSum[i]);              //每一個字首和都是一個子陣列和
            for(int j = i + 1; j < size; ++j) {
                sums.push_back(preSum[j] - preSum[i]);     //列舉所有起點和終點,計運算元陣列的和
            }
        }
        //for(auto x : sums) cout << x << ' ';
        sort(sums.begin(), sums.end());
        bignum res = 0;
        for(int i = left - 1; i <= right - 1; ++i) {
            res += sums[i];          
        }
        return res % mod;
    }
};

最後一個for迴圈也可以這麼寫:

for(int i = left - 1; i <= right - 1; ++i) {
            res += sums[i];
            if(res >= mod) {
                res -= mod;
            }
        }
        return res;

5446. 三次操作後最大值與最小值的最小差

這是一道智商題,如果能發現規律就很好做。

要求出三次操作後最大值和最小值的差的最小值。
我們就要讓三次操作之後最大值和最小值儘可能接近,也就是儘可能讓最大值小一點,最小值大一點。

所以三次操作都是對陣列的最大值(或者前兩個或前三個最大值)或者對陣列的最小值(或者前兩個或前三個最小值)進行操作。
這一點很好理解,如果對一箇中間元素進行操作,則對於最大值和最小值的差並無影響。

由於只有三次操作,且都是對於最小或最大的幾個數進行操作,所以一共只有四種情況:

  1. 三次操作將最小的三個數改為第四小的數。 修改最小的三個數,修改後的值一定是改為第四小的數,
    因為這樣才會讓最大值和最小值(原來的第四小的值)的差最小。

  2. 兩次操作將最小的兩個數改為第三小的數,一次操作將最大的數改為第二小的數。 這也是一種縮小最大值和最小值的差的方法。

  3. 一次操作將最小的數改為第二小的數,兩次操作將最大的兩個數改為第三大的數。

  4. 三次操作將最大的三個數改為第四大的數。

最終的結果,就是上述四種情況的最小值!

注意,如果一共只有四個數或者更少,直接返回0即可,因為肯定可以讓最大值和最小值的差相等。

程式碼如下:

class Solution {
public:
    int minDifference(vector<int>& nums) {
        int size = nums.size();
        if(size <= 4) {
            return 0;
        }
        sort(nums.begin(), nums.end());
        int res = INT_MAX;
        for(int i = 0; i <= 3; ++i) {            //列舉四種情況
            res = min(res, nums[size - 4 + i] - nums[i]);    //更新修改後的最大值和最小值的差的最小值
        }
        return res;
    }
};

5447. 石子游戲 IV


題意是給定石頭數量,alice和bob每次都只能拿平方數個石頭,如果某次操作某人拿走了所有的石頭,則另一個玩家輸。
典型DP問題: 用一個數組dp表示石頭數和alice是否穩贏的關係,比如dp[i]表示有i個石頭時alice是否穩贏(是返回true,否返回false)。

  1. 顯然,當i是0時,alice是輸得,因為題目說了沒有石頭時,無法操作的玩家輸。

  2. 當i是一個平方數時,aclie穩贏,因為只要alice把石頭全部拿走,bob就輸了。

  3. 對於其他情況,就需要判斷從當前石頭裡拿走所有可以拿的平方數個石頭的方案裡,對方是否一定輸。
    只要有一種拿平方數的方案裡,對方一定輸,就可以返回true,否則,遍歷完所有可以拿的方案對方都不輸,就返回false。

程式碼如下:

class Solution {
public:
    bool winnerSquareGame(int n) {
        vector<bool> dp(n + 1);                  //dp[i]表示有i個石頭時alice是否穩贏
        for(int i = 0; i <= n; ++i) {            //從小到大遞推計算dp陣列
            for(int j = 1; j * j + i <= n; ++j) {     //列舉可以拿的石頭數,j * j是當前拿的石頭數
                if(dp[i] == false) {                //如果只有i個石頭時必輸
                    dp[j * j + i] = true;           //那麼有j * j + i個石頭的時候就必贏,因為alice可以拿走j * j個石頭,這樣bob就必輸
                }
            }
        }
        return dp[n];         //返回有n個石頭時alice是否贏
    }
};