1. 程式人生 > >今日頭條2018校園招聘後端開發工程師(第四批)程式設計題

今日頭條2018校園招聘後端開發工程師(第四批)程式設計題

這一場題目還是挺好玩的,也挺有技巧的,這樣的題目做起來才有意思。

原題連結:點這兒

第一題:程式設計題1

題目:

>有三隻球隊,每隻球隊編號分別為球隊1,球隊2,球隊3,這三隻球隊一共需要進行n 場比賽。現在已經踢完了k場比賽,每場比賽不能打平,踢贏一場比賽得一分,輸了不得分不減分。已知球隊1和球隊2的比分相差d1分,球隊2和球隊3的比分相差d2分,每場比賽可以任意選擇兩隻隊伍進行。求如果打完最後的 (n-k)場比賽,有沒有可能三隻球隊的分數打平。



輸入:

第一行包含一個數字 t(1<=t<=10)

接下來的

t行每行包括四個數字 n,k,d1,d2(1<=n<=1012;0<=k<=n,0<=d1,d2<=k)

輸出:

每行的比分資料,最終三隻球隊若能夠打平,則輸出“yes”,否則輸出“no”

樣例輸入:

2
3 3 0 0
3 3 3 3

樣例輸出:

yes
no

解析

還是要先分析下題目,注意題中加粗的字型,這意味著,所有的比賽總分加起來一定是n,打了k場比賽,那麼這k長比賽的總分是k

現在相當於你是裁判,你想讓那個對贏那個隊就贏,前提是你在其餘兩個隊中隨便選一個隊作為炮灰和贏的那個隊打。

而知道的資訊只有相鄰兩個隊的比分差距,因此,這裡可以假設第一個隊的比分為x,那麼第二個隊的比分就可能是x+d1xd1,那麼第三個隊的成績就可以由第二個隊的成績得到,可能是x+d1+d2x+d1d2xd1+d2xd1d2

總共是4中情況,因此列舉這四種情況可以得到三個隊打了k場比賽的比分a, b, c(a+b+c=k)

那麼現在問題就轉化成了,你有n個硬幣,拿出了k個硬幣,分成了數量為a, b, c的三堆。現在,你要把剩下的n - k個硬幣分到這三個堆中,讓三個堆的硬幣數量相等。看下圖。

這裡寫圖片描述

a, b, c要滿足一下條件:0 <= a, b, c <= k並且剩下的n - k個硬幣要先把白陰影部分填滿了,然後剩下的那些硬幣要剛好填滿黑陰影部分,只有這樣,三個隊才有可能分數一樣。

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

void getabc(int i, LL a, LL *pb, LL *pc, LL d1, LL d2)
{
    switch (i) {
    case 0:
        *pb = a + d1;
        *pc = *pb + d2;
        break;
    case 1:
        *pb = a + d1;
        *pc = *pb - d2;
        break;
    case 2:
        *pb = a - d1;
        *pc = *pb + d2;
        break;
    case 3:
        *pb = a - d1;
        *pc = *pb - d2;
        break;
    }
}

int main()
{
    int T;
    for (cin >> T; T--; ) {
        LL n, k, d1, d2;
        scanf("%lld%lld%lld%lld", &n, &k, &d1, &d2);

        LL x[] = {k - 2 * d1 - d2,
                k - 2 * d1 + d2,
                k + 2 * d1 - d2,
                k + 2 * d1 + d2};
        bool ans = false;
        for (int i = 0; i < 4; i++) {
            int sum = 0;
            for (LL t = x[i]; t; sum += t % 10, t /= 10) {}

            if (sum % 3 == 0 && 0 <= x[i] / 3 && x[i] / 3 <= k) {
                LL a = x[i] / 3, b, c;
                getabc(i, a, &b, &c, d1, d2);

                if (0 <= b && b <= k && 0 <= c && c <= k) {
                    LL max_ele = max(a, max(b, c));
                    LL diff = 3 * max_ele - a - b - c;
                    if (n - k - diff >= 0 && (n - k - diff) % 3 == 0) {
                        ans = true;
                        break;
                    }
                }
            }
        }
        cout << (ans ? "yes" : "no") << endl;
    }
    return 0;
}

上面的這個程式碼和思路是我一開始的想法,但是後來一想,如果n不是3的倍數,那麼無論怎麼搞都不可能存在三個隊分數一樣的情況;看上圖,從圖中可以看出,如果你得到的a, b, c都小於或等於n3n是3的倍數,那麼你把剩下的n - k個硬幣依次丟到三個堆中,直到a, b, c等於n3,這個時候絕對沒有硬幣剩下!不信你可以試試。

下面的這個程式碼是最終程式碼。

如果你不熟悉這三個函式,那麼你自己可以手寫一個求上界的二分,一個求下界的二分,具體可以參考你真的理解二分的寫法嗎 - 二分寫法詳解

程式碼

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

void getabc(int i, LL a, LL *pb, LL *pc, LL d1, LL d2)
{
    switch (i) {
    case 0:
        *pb = a + d1;
        *pc = *pb + d2;
        break;
    case 1:
        *pb = a + d1;
        *pc = *pb - d2;
        break;
    case 2:
        *pb = a - d1;
        *pc = *pb + d2;
        break;
    case 3:
        *pb = a - d1;
        *pc = *pb - d2;
        break;
    }
}

int main()
{
    int T;
    for (cin >> T; T--; ) {
        LL n, k, d1, d2;
        scanf("%lld%lld%lld%lld", &n, &k, &d1, &d2);
        if (n % 3) {
            cout << "no" << endl;
            continue;
        }

        LL x[] = {k - 2 * d1 - d2,
                    k - 2 * d1 + d2,
                    k + 2 * d1 - d2,
                    k + 2 * d1 + d2};
        bool ans = false;
        for (int i = 0; i < 4; i++) {
            int sum = 0;
            for (LL t = x[i]; t; sum += t % 10, t /= 10) {}

            if (sum % 3 == 0 && 0 <= x[i] / 3 && x[i] / 3 <= k && x[i] <= n) {
                LL a = x[i] / 3, b, c;
                getabc(i, a, &b, &c, d1, d2);

                if (0 <= b && b <= min(k, n / 3) && 0 <= c && c <= min(k, n / 3)) {
                    ans = true;
                    break;
                }
            }
        }
        cout << (ans ? "yes" : "no") << endl;
    }
    return 0;
}

第二題:程式設計題2

題目:

有一個僅包含’a’’b’兩種字元的字串s,長度為n,每次操作可以把一個字元做一次轉換(把一個’a’設定為’b’,或者把一個’b’置成’a’);但是操作的次數有上限m,問在有限的運算元範圍內,能夠得到最大連續的相同字元的子串的長度是多少。

輸入:

第一行兩個整數 n,m(1<=m<=n<=50000),第二行為長度為n且只包含’a’’b’的字串s

輸出:

輸出在操作次數不超過 m 的情況下,能夠得到的 最大連續 全’a’子串或全’b’子串的長度。

樣例輸入:

8 1
aabaabaa

樣例輸出:

5

解析

這個題和第二批今日頭條2018校園招聘後端開發工程師(第二批)程式設計題 - 題解中的第三題:字母交換很相似啊。於是,順手就寫了一個區間動態規劃,然後提交了一下,發現記憶體爆,於是,把dp陣列的意義改了一下,從dp[i][j]表示區間[i, j]需要修改的最少次數變成了dp[i][j]表示區間[i, j]表示以j為左端點,區間長度為i的區間需要修改的最少次數。這個時候就可以使用滾動陣列來優化儲存空間了。

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int n, m;
    string s;
    for (; cin >> n >> m >> s; ) {
        int ans = 0;
        for (char c = 'a'; c < 'c'; ++c) {
            vector<vector<int> > dp(2, vector<int>(s.size(), 0));
            int ret = 1;
            for (int i = 0; i < (int)s.size(); ++i)
                dp[1][i] = s[i] != c;
            for (int len = 2; len <= (int)s.size(); ++len) {
                for (int i = 0; i + len - 1 < (int)s.size(); i++) {
                    dp[len % 2][i] = max((dp[(len - 1) % 2][i] + (s[i + len - 1] != c)),
                                         (dp[(len - 1) % 2][i + 1] + (s[i] != c)));
                    if (dp[len % 2][i] <= m)
                        ret = len;
                }
            }
            ans = max(ans, ret);
        }
        cout << ans << endl;
    }
    return 0;
}

提交了一下這個程式碼,發現超時了,這個時候才注意到n,m(1<=m<=n<=50000),而這個區間動態規劃的時間複雜度為O(n2),這個複雜度太高了;

動態規劃其實屬於優雅一點的暴力解法,那麼遇到複雜度O(n2)的演算法,第一時間要想到優化成O(nlogn),這一點在我之前的博文中反覆提到,因為你這樣分析可以為你指明下一步的思考方向,反正我現在就形成了這樣的思維。

注意到,上面這個動態規劃從小到大枚舉了區間長度,而且仔細想一想,求區間[i, j]中最少需要的修改次數並不需要子區間的最優解,可以直接看看區間[i, j]中有多少不是目標字母的字母,而這個數量可以通過字首和來得到。

於是結合上面兩點,可以得到下面的優化演算法:二分列舉答案(連續區間長度),然後在序列中遍歷所有區間長度為指定長度的子區間(字首和可以的到最少修改次數)。這樣一來,演算法的時間複雜度就是O(nlogn)了。

還要注意,二分答案的時候,求的是上界,那麼二分的端點l, r的初始取值以及mid的取值要寫對,不會寫的可以看下這篇部落格你真的理解二分的寫法嗎 - 二分寫法詳解。反正我以前不理解二分的時候我就會瞎蒙,但是自從思考清楚寫下這篇部落格後,二分求上下界,那是板上釘釘,很快很準確就搞出來了。

程式碼

#include <bits/stdc++.h>

using namespace std;

int main()
{
    int n, m;
    string s;
    for (; cin >> n >> m >> s; ) {
        int ans = 0;
        for (char c = 'a'; c < 'c'; ++c) {
            vector<int> dp(s.size() + 1, 0);
            for (int i = 1; i <= (int)s.size(); ++i)
                dp[i] = dp[i - 1] + (s[i - 1] == c);
            int l = -1, r = (int)s.size() - 1;
            while (l < r) {
                int mid = (l + r + 1) / 2;
                bool f = false;
                for (int i = 0; i + mid - 1 < (int)s.size(); i++)
                    if (dp[i + mid] - dp[i] <= m) {
                        f = true;
                        break;
                    }
                f ? l = mid : r = mid - 1;
            }
            ans = max(ans, r);
        }
        cout << ans << endl;
    }
    return 0;
}

第三題:附加題

題目:

存在n+1個房間,每個房間依次為房間1 2 3...i,每個房間都存在一個傳送門,i房間的傳送門可以把人傳送到房間pi(1<=pi<=i),現在路人甲從房間1開始出發(當前房間1即第一次訪問),每次移動他有兩種移動策略:

  • A、如果訪問過當前房間i偶數次,那麼下一次移動到房間i+1
  • B、如果訪問過當前房間i奇數次,那麼移動到房間pi

現在路