做过第三批的题目,今日头条2018校园招聘后端开发工程师(第三批)编程题 - 题解和第二批的题目,今日头条2018校园招聘后端开发工程师(第二批)编程题 - 题解。
这一场题目还是挺好玩的,也挺有技巧的,这样的题目做起来才有意思。
原题链接:点这儿。
第一题:编程题1
题目:
>有三只球队,每只球队编号分别为球队1
,球队2
,球队3
,这三只球队一共需要进行n
场比赛。现在已经踢完了k
场比赛,每场比赛不能打平,踢赢一场比赛得一分,输了不得分不减分。已知球队1
和球队2
的比分相差d1
分,球队2
和球队3
的比分相差d2
分,每场比赛可以任意选择两只队伍进行。求如果打完最后的 (n-k)
场比赛,有没有可能三只球队的分数打平。
输入:
第一行包含一个数字 t(1<=t<=10) t ( 1 <= t <= 10 )
接下来的 t t 行每行包括四个数字
输出:
每行的比分数据,最终三只球队若能够打平,则输出
“yes”
,否则输出“no”
样例输入:
2 3 3 0 0 3 3 3 3
样例输出:
yes no
解析
还是要先分析下题目,注意题中加粗的字体,这意味着,所有的比赛总分加起来一定是n
,打了k
场比赛,那么这k
长比赛的总分是k
;
现在相当于你是裁判,你想让那个对赢那个队就赢,前提是你在其余两个队中随便选一个队作为炮灰和赢的那个队打。
而知道的信息只有相邻两个队的比分差距,因此,这里可以假设第一个队的比分为 x x ,那么第二个队的比分就可能是或 x−d1 x − d 1 ,那么第三个队的成绩就可以由第二个队的成绩得到,可能是 x+d1+d2 x + d 1 + d 2 、 x+d1−d2 x + d 1 − d 2 、 x−d1+d2 x − d 1 + d 2 、 x−d1−d2 x − d 1 − d 2 ;
总共是4中情况,因此枚举这四种情况可以得到三个队打了k
场比赛的比分a, b, c
(a+b+c=k)
(
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
都小于或等于
n3
n
3
且n
是3的倍数,那么你把剩下的n - k
个硬币依次丢到三个堆中,直到a, b, c
等于
n3
n
3
,这个时候绝对没有硬币剩下!不信你可以试试。
下面的这个代码是最终代码。
如果你不熟悉这三个函数,那么你自己可以手写一个求上界的二分,一个求下界的二分,具体可以参考你真的理解二分的写法吗 - 二分写法详解。
代码
#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 , 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) n , m ( 1 <= m <= n <= 50000 ) ,而这个区间动态规划的时间复杂度为 O(n2) O ( n 2 ) ,这个复杂度太高了;
动态规划其实属于优雅一点的暴力解法,那么遇到复杂度 O(n2) O ( n 2 ) 的算法,第一时间要想到优化成 O(nlogn) O ( n l o g n ) ,这一点在我之前的博文中反复提到,因为你这样分析可以为你指明下一步的思考方向,反正我现在就形成了这样的思维。
注意到,上面这个动态规划从小到大枚举了区间长度,而且仔细想一想,求区间[i, j]
中最少需要的修改次数并不需要子区间的最优解,可以直接看看区间[i, j]
中有多少不是目标字母的字母,而这个数量可以通过前缀和来得到。
于是结合上面两点,可以得到下面的优化算法:二分枚举答案(连续区间长度),然后在序列中遍历所有区间长度为指定长度的子区间(前缀和可以的到最少修改次数)。这样一来,算法的时间复杂度就是 O(nlogn) O ( n l o g n ) 了。
还要注意,二分答案的时候,求的是上界,那么二分的端点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) p i ( 1 <= p i <= i ) ,现在路人甲从房间1
开始出发(当前房间1
即第一次访问),每次移动他有两种移动策略:
- A、如果访问过当前房间
i
偶数次,那么下一次移动到房间i+1
;- B、如果访问过当前房间
i
奇数次,那么移动到房间 pi p i ;现在路人甲想知道移动到房间
n+1
一共需要多少次移动。
输入:
第一行包括一个数字
n
(30%数据 1<=n<=100 1 <= n <= 100 ,100%数据 1<=n<=1000 1 <= n <= 1000 ),表示房间的数量,接下来一行存在n
个数字 pi(1<=pi<=i) p i ( 1 <= p i <= i ) , pi p i 表示从房间i可以传送到房间 pi p i 。
输出:
输出一行数字,表示最终移动的次数,最终结果需要对
1000000007
取模。
样例输入:
2 1 2
样例输出:
4
解析
这个题目有意思,一开始我也没想到做法,但是后面注意到了这个条件, pi(1<=pi<=i) p i ( 1 <= p i <= i ) ,才想到这是个动态规划。
有了上面这个条件为什么就能用动态规划做了呢?因为满足了最优子结构。这个条件保证了可以利用子结构的最优解。
先来分析一下, pi(1<=pi<=i) p i ( 1 <= p i <= i ) ,这句话意味着传送门不可能把你往前面的门传,如果你想向前走,那么你只能访问该房间偶数次;
假设你现在第一次到达i
门,你觉得前面i - 1
个房子你都访问了多少次?每个房子访问了多少次我不知道,但是我知道每个房子访问的次数都是偶数!这一点很重要,不然写不出状态转移方程;这是为什么呢,其实答案就在上一段话,仔细想想,假如前面i - 1
中有一个房子的访问次数不是偶数次,那么,你不可能向前走,更不可能走到i
门。
想清楚了这一点,动态规划方程很好写了,设dp[i]
为到达i
门,并且进入次数为偶数时需要移动的次数,看下图:
要进入i
门,那么就要从i - 1
门过来,故访问i - 1
门的次数一定为偶数,故dp[i] = dp[i - 1] + 1
;
这个时候到达了i
门,由于第一次进入,故次数为奇数,因此被送会pos[i]
门,dp[i] += 1
;
这个时候到达pos[i]
门,由于之前到达pos[i]
门的次数为偶数,因此这次到达的次数就是奇数,故从pos[i]
门走到i - 1
个门,并且到达i - 1
门的次数为偶数的移动次数就是红色部分;
红色部分怎么求呢,首先,黑色部分是dp[i - 1]
,黄色部分是dp[pos[i] - 1]
,红色部分就是red = dp[i - 1] - dp[pos[i] - 1] - 1
,这里注意,红色部分表示的是值,而不是人移动的范围(实际上人还可以移动到黄色部分去,但最终还是会移动到红色部分的右端点)。dp[i] += red
;
移动到了i - 1
门,且访问次数为偶数次,那么下一步就会移动到i
门,且这个时候i
门的访问次数为偶数次,因此dp[i] += 1
。
综上,dp[i] = 2 * dp[i - 1] + 2
。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL mod = 1000000007;
int main()
{
int n;
while (cin >> n) {
vector<int> arr(n + 1, 0);
vector<LL> dp(n + 1, 0);
for (int i = 1, x; i <= n; cin >> x, arr[i++] = x) {}
if (n == 1) {
cout << "1" << endl;
continue;
}
for (int i = 1; i <= n; i++)
dp[i] = (2 * dp[i - 1] % mod - dp[arr[i] - 1] + 2) % mod;
cout << dp[n] % mod << endl;
}
return 0;
}