今日頭條2018校園招聘後端開發工程師(第四批)程式設計題
這一場題目還是挺好玩的,也挺有技巧的,這樣的題目做起來才有意思。
原題連結:點這兒。
第一題:程式設計題1
題目:
>有三隻球隊,每隻球隊編號分別為球隊1
,球隊2
,球隊3
,這三隻球隊一共需要進行n
場比賽。現在已經踢完了k
場比賽,每場比賽不能打平,踢贏一場比賽得一分,輸了不得分不減分。已知球隊1
和球隊2
的比分相差d1
分,球隊2
和球隊3
的比分相差d2
分,每場比賽可以任意選擇兩隻隊伍進行。求如果打完最後的 (n-k)
場比賽,有沒有可能三隻球隊的分數打平。
輸入:
第一行包含一個數字
接下來的
行每行包括四個數字
輸出:
每行的比分資料,最終三隻球隊若能夠打平,則輸出
“yes”
,否則輸出“no”
樣例輸入:
2 3 3 0 0 3 3 3 3
樣例輸出:
yes no
解析
還是要先分析下題目,注意題中加粗的字型,這意味著,所有的比賽總分加起來一定是n
,打了k
場比賽,那麼這k
長比賽的總分是k
;
現在相當於你是裁判,你想讓那個對贏那個隊就贏,前提是你在其餘兩個隊中隨便選一個隊作為炮灰和贏的那個隊打。
而知道的資訊只有相鄰兩個隊的比分差距,因此,這裡可以假設第一個隊的比分為,那麼第二個隊的比分就可能是或,那麼第三個隊的成績就可以由第二個隊的成績得到,可能是、、、;
總共是4中情況,因此列舉這四種情況可以得到三個隊打了k
場比賽的比分a, b, c
。
那麼現在問題就轉化成了,你有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
都小於或等於且n
是3的倍數,那麼你把剩下的n - k
個硬幣依次丟到三個堆中,直到a, b, c
等於,這個時候絕對沒有硬幣剩下!不信你可以試試。
下面的這個程式碼是最終程式碼。
如果你不熟悉這三個函式,那麼你自己可以手寫一個求上界的二分,一個求下界的二分,具體可以參考你真的理解二分的寫法嗎 - 二分寫法詳解。
程式碼
#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
且只包含’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;
}
提交了一下這個程式碼,發現超時了,這個時候才注意到,而這個區間動態規劃的時間複雜度為,這個複雜度太高了;
動態規劃其實屬於優雅一點的暴力解法,那麼遇到複雜度的演算法,第一時間要想到優化成,這一點在我之前的博文中反覆提到,因為你這樣分析可以為你指明下一步的思考方向,反正我現在就形成了這樣的思維。
注意到,上面這個動態規劃從小到大枚舉了區間長度,而且仔細想一想,求區間[i, j]
中最少需要的修改次數並不需要子區間的最優解,可以直接看看區間[i, j]
中有多少不是目標字母的字母,而這個數量可以通過字首和來得到。
於是結合上面兩點,可以得到下面的優化演算法:二分列舉答案(連續區間長度),然後在序列中遍歷所有區間長度為指定長度的子區間(字首和可以的到最少修改次數)。這樣一來,演算法的時間複雜度就是了。
還要注意,二分答案的時候,求的是上界,那麼二分的端點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房間的傳送門可以把人傳送到房間,現在路人甲從房間1
開始出發(當前房間1
即第一次訪問),每次移動他有兩種移動策略:
- A、如果訪問過當前房間
i
偶數次,那麼下一次移動到房間i+1
;- B、如果訪問過當前房間
i
奇數次,那麼移動到房間;現在路