今日头条2018校园招聘后端开发工程师(第四批)编程题 - 题解

做过第三批的题目,今日头条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 行每行包括四个数字 n,k,d1,d2(1<=n<=1012;0<=k<=n,0<=d1,d2<=k) n , k , d 1 , d 2 ( 1 <= n <= 10 12 ; 0 <= k <= n , 0 <= d 1 , d 2 <= k )

输出:

每行的比分数据,最终三只球队若能够打平,则输出“yes”,否则输出“no”

样例输入:

2
3 3 0 0
3 3 3 3

样例输出:

yes
no

解析

还是要先分析下题目,注意题中加粗的字体,这意味着,所有的比赛总分加起来一定是n,打了k场比赛,那么这k长比赛的总分是k

现在相当于你是裁判,你想让那个对赢那个队就赢,前提是你在其余两个队中随便选一个队作为炮灰和赢的那个队打。

而知道的信息只有相邻两个队的比分差距,因此,这里可以假设第一个队的比分为 x x ,那么第二个队的比分就可能是 x+d1 x + d 1 xd1 x − d 1 ,那么第三个队的成绩就可以由第二个队的成绩得到,可能是 x+d1+d2 x + d 1 + d 2 x+d1d2 x + d 1 − d 2 xd1+d2 x − d 1 + d 2 xd1d2 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 

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 

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 

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 

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 

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 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;
}

你可能感兴趣的:(笔试面试题,笔试面试题)