【补题】网易 2018 校招笔试编程题 - 解题思路

补题补题

牛客网给出的题解:https://www.nowcoder.com/discuss/39219
我自己目前只有前 7 题,后面一题先放着。我自己的解析也发在牛客网的题目下了。。不知能不能骗骗赞 233

魔法币

Description

小易准备去魔法王国采购魔法神器,购买魔法神器需要使用魔法币,但是小易现在一枚魔法币都没有,但是小易有两台魔法机器可以通过投入x(x可以为0)个魔法币产生更多的魔法币。
魔法机器1:如果投入x个魔法币,魔法机器会将其变为2x+1个魔法币
魔法机器2:如果投入x个魔法币,魔法机器会将其变为2x+2个魔法币
小易采购魔法神器总共需要n个魔法币,所以小易只能通过两台魔法机器产生恰好n个魔法币,小易需要你帮他设计一个投入方案使他最后恰好拥有n个魔法币。

Input

输入包括一行,包括一个正整数n(1 ≤ n ≤ 10^9),表示小易需要的魔法币数量。

Output

输出一个字符串,每个字符表示该次小易选取投入的魔法机器。其中只包含字符'1'和'2'。

Sample Input

10

Sample Output

122

解题思路

水题,求 x 经过若干次 \(f(x)=2x+c\) 的变换恰好到 n ,显然次数不超过 \(log(n)+1=32\) 次,而 \(c=1 || 2\) 使得每次 n' 具有奇偶特征,直接递归出来。

AC 代码

#include <stdio.h>

int n;

void read() {
    scanf("%d", &n);
}

void magic(int n) {
    if (n <= 0) return;
    if (n & 1) {
        magic((n - 1) / 2);
        putchar('1');
    } else {
        magic((n - 2) / 2);
        putchar('2');
    }
}

void work() {
    magic(n);
    putchar('\n');
}

int main() {
    read();
    work();
    return 0;
}

相反数

Description

为了得到一个数的"相反数",我们将这个数的数字顺序颠倒,然后再加上原先的数得到"相反数"。例如,为了得到1325的"相反数",首先我们将该数的数字顺序颠倒,我们得到5231,之后再加上原先的数,我们得到5231+1325=6556.如果颠倒之后的数字有前缀零,前缀零将会被忽略。例如n = 100, 颠倒之后是1.

Input

输入包括一个整数n,(1 ≤ n ≤ 10^5)

Output

输出一个整数,表示n的相反数

Sample Input

1325

Sample Output

6556

解题思路

水题,可以算程序设计入门课作业题。

AC 代码

#include <stdio.h>

int n;

void read() {
    scanf("%d", &n);
}

int reverse(int n) {
    int res = 0;
    while (n) {
        res = res * 10 + n % 10;
        n /= 10;
    }
    return res;
}

void work() {
    printf("%d\n", n + reverse(n));
}

int main() {
    read();
    work();
    return 0;
}

字符串碎片

Description

一个由小写字母组成的字符串可以看成一些同一字母的最大碎片组成的。例如,"aaabbaaac"是由下面碎片组成的:'aaa','bb','c'。牛牛现在给定一个字符串,请你帮助计算这个字符串的所有碎片的平均长度是多少。

Input

输入包括一个字符串s,字符串s的长度length(1 ≤ length ≤ 50),s只含小写字母('a'-'z')

Output

输出一个整数,表示所有碎片的平均长度,四舍五入保留两位小数。

如样例所示: s = "aaabbaaac"
所有碎片的平均长度 = (3 + 2 + 3 + 1) / 4 = 2.25

Sample Input

aaabbaaac

Sample Output

2.25

解题思路

水题,所有块的总长度就是字符串的长度,数一下有多少块。

AC 代码

#include <stdio.h>

char str[55];

void read() {
    scanf("%s", str);
}

void work() {
    int cnt = 1, i;
    for (i = 1; str[i]; ++i) {
        if (str[i] != str[i-1]) {
            ++cnt;
        }
    }
    double res = (double)(i) / cnt;
    printf("%.2lf\n", res);
}

int main() {
    read();
    work();
    return 0;
}

游历魔法王国

Description

魔法王国一共有n个城市,编号为0~n-1号,n个城市之间的道路连接起来恰好构成一棵树。
小易现在在0号城市,每次行动小易会从当前所在的城市走到与其相邻的一个城市,小易最多能行动L次。
如果小易到达过某个城市就视为小易游历过这个城市了,小易现在要制定好的旅游计划使他能游历最多的城市,请你帮他计算一下他最多能游历过多少个城市(注意0号城市已经游历了,游历过的城市不重复计算)。

Input

输入包括两行,第一行包括两个正整数n(2 ≤ n ≤ 50)和L(1 ≤ L ≤ 100),表示城市个数和小易能行动的次数。
第二行包括n-1个整数parenti, 对于每个合法的i(0 ≤ i ≤ n - 2),在(i+1)号城市和parent[i]间有一条道路连接。

Output

输出一个整数,表示小易最多能游历的城市数量。

Sample Input

5 2
0 1 2 3

Sample Output

3

解题思路

原本以为是树上 dp ,其实是贪心。

画个图可以知道,可把 parent[i] 当作 (i+1) 的父亲节点(因为 parent[i] 是可以重复的)。之前看漏了 parent[i] 的范围限制了父节点标号比子节点小 这个条件,我用了 链式前向星 来建图。

建好图之后,就可以从树根扩散出每个节点所在最长树链的长度,选出最长的一条树链,记其长度为 maxLen 。

分类讨论:

  • 若 L ≤ maxLen ,显而易见得结果;
  • 若 L > maxLen ,意味着可以往回走,要知道越短的树链往回走的代价越低。如果从末端往回走,消耗的代价非常高,最坏情况是较短的树链都连接在最远的树根上,整条最长链都要回走;如果已经知道最终步数会有剩余,则可以先消耗富余的步数走短链,最后才走最长链;
  • 继续对 rest = L - maxLen 进行讨论:
    • 若树链上存在某个节点拥有另一条子链,其长度 x 必定小于或等于该祖先到原链末端的长度,考察树链上每个节点到叶子的一条最短子链
      • 当 x > rest/2 可以在中途预先用掉 rest 步而不影响要走的 maxLen 最长链,可达城市增加 rest/2 个;
      • 当 x ≤ rest/2 可以在中途预先用掉 2x 步而不影响要走的 maxLen 最长链,可达城市增加 x 个;
      • 当所有的 x 总和 sum(x) ≤ rest/2 说明富余的步数足够把最短链到次最长链都走一遍,可达城市为全部 n 个。
    • 本小节讨论可知 rest/2 决定了能多走的城市数量,总共能走 min(n, 1 + rest/2 + maxLen) 个城市。

AC 代码

#include <stdio.h>
#include <string.h>

#define MAXN 55
#define MAXM 55

inline void getMax(int& n, int x) {
    n < x && (n = x);
}

inline void getMin(int& n, int x) {
    n > x && (n = x);
}

struct Edge {
    int to;
    int next;
} edge[MAXM];

int cnt;
int head[MAXN], len[MAXN];

void init() {
    memset(head, 0xff, sizeof(head));
}

void addEdge(int u, int v) {
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    head[u] = cnt++;
}

int n, L;

void read() {
    int parent;
    scanf("%d%d", &n, &L);
    for (int i = 1; i < n; ++i) {
        scanf("%d", &parent);
        addEdge(parent, i);
    }
}

void walk(int u) {
    for (int i = head[u]; ~i; i = edge[i].next) {
        len[edge[i].to] = len[u] + 1;
        walk(edge[i].to);
    }
}

void work() {
    walk(0);
    int maxLen = 0;
    for (int i = 0; i < n; ++i) {
        getMax(maxLen, len[i]);
    }
    if (L <= maxLen) {
        printf("%d\n", L + 1);
    } else {
        int res = n;
        getMin(res, maxLen + (L - maxLen) / 2 + 1);
        printf("%d\n", res);
    }
}

int main() {
    init();
    read();
    work();
    return 0;
}

重排数列

Description

小易有一个长度为N的正整数数列A = {A[1], A[2], A[3]..., A[N]}。
牛博士给小易出了一个难题:
对数列A进行重新排列,使数列A满足所有的A[i] * A[i + 1] (1 ≤ i ≤ N - 1)都是4的倍数。
小易现在需要判断一个数列是否可以重排之后满足牛博士的要求。

Input

输入的第一行为数列的个数t(1 ≤ t ≤ 10),
接下来每两行描述一个数列A,第一行为数列长度n(1 ≤ n ≤ 10^5)
第二行为n个正整数Ai

Output

对于每个数列输出一行表示是否可以满足牛博士要求,如果可以输出Yes,否则输出No。

Sample Input

2
3
1 10 100
4
1 2 3 4

Sample Output

Yes
No

解题思路

分类讨论下。

  • 显然,任意数和 4 的倍数相乘,其结果仍是 4 的倍数;
  • 显然,若存在任意数量 2 的倍数,两两之间乘起来就是 4 的倍数;
  • 如果存在一个数不是 2 的倍数,即它是一个奇数:
    • 放在 2 的倍数旁边,一定不符合要求;
    • 放在 4 的倍数旁边,相乘结果仍是 4 的倍数。

因此符合要求的排列分两种情况:

  1. 存在 2 的倍数,所有 2 的倍数相邻排列,需要一个 4 的倍数连接剩下的数,奇数最多和 4 的倍数数量相等,要求 countMod4 >= countOdd
  2. 没有 2 的倍数,原本放 2 的倍数一端可以改放一个奇数,countMod4 >= countOdd - 1

AC 代码

#include <stdio.h>

int n;
int arr[100100];

int countMod4, countMod2;

void read() {
    countMod4 = 0;
    countMod2 = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf("%d", arr + i);
        if (arr[i] % 4 == 0) {
            ++countMod4;
        } else if (arr[i] % 2 == 0) {
            ++countMod2;
        }
    }
}

void work() {
    int countOdd = n - countMod4 - countMod2;
    if ((n == 1 && countMod4) || countMod4 >= countOdd - !countMod2) {
        puts("Yes");
    } else {
        puts("No");
    }
}

int main() {
    int t;
    scanf("%d", &t);
    while (t--) {
        read();
        work();
    }
    return 0;
}

最长公共子括号序列

Description

一个合法的括号匹配序列被定义为:

  1. 空串""是合法的括号序列
  2. 如果"X"和"Y"是合法的序列,那么"XY"也是一个合法的括号序列
  3. 如果"X"是一个合法的序列,那么"(X)"也是一个合法的括号序列
  4. 每个合法的括号序列都可以由上面的规则生成
    例如"", "()", "()()()", "(()())", "(((()))"都是合法的。
    从一个字符串S中移除零个或者多个字符得到的序列称为S的子序列。
    例如"abcde"的子序列有"abe","","abcde"等。
    定义LCS(S,T)为字符串S和字符串T最长公共子序列的长度,即一个最长的序列W既是S的子序列也是T的子序列的长度。
    小易给出一个合法的括号匹配序列s,小易希望你能找出具有以下特征的括号序列t:
    1、t跟s不同,但是长度相同
    2、t也是一个合法的括号匹配序列
    3、LCS(s, t)是满足上述两个条件的t中最大的
    因为这样的t可能存在多个,小易需要你计算出满足条件的t有多少个。

如样例所示: s = "(())()",跟字符串s长度相同的合法括号匹配序列有:
"()(())", "((()))", "()()()", "(()())",其中LCS( "(())()", "()(())" )为4,其他三个都为5,所以输出3.

Input

输入包括字符串s(4 ≤ |s| ≤ 50,|s|表示字符串长度),保证s是一个合法的括号匹配序列。

Output

输出一个正整数,满足条件的t的个数。

Sample Input

(())()

Sample Output

3

解题思路

根据题意,当且仅当修改距离为 1 时 LCS 最大。很容易证明对于两种基本序列 (()) 和 ()() 都有距离为 1 的合法修改。
原本想的是对每个左括号,跟每个右括号替换,判断合法后累计。
后来发现会漏掉一些情况,那就暴力得干脆一点,把每个符号插入到任意位置,判合法,去重,累计。

AC 代码

#include <stdio.h>
#include <algorithm>
#include <string>
#include <set>

using namespace std;

char str[55];

void read() {
    scanf("%s", str);
}

bool test(const string& s) {
    int cnt = 0;
    for (int i = 0; s[i]; ++i) {
        s[i] == '(' ? ++cnt : --cnt;
        if (cnt < 0) {
            return false;
        }
    }
    return true;
}

void work() {
    set<string> record;
    for (int i = 1; str[i+1]; ++i) {
        string tmp(str);
        tmp.erase(i, 1);
        for (int j = 1; str[j]; ++j) {
            if (str[i] == str[j]) continue;
            string s(tmp);
            s.insert(j, 1, str[i]);
            if (test(s)) {
                record.insert(s);
            }
        }
    }
    printf("%lu\n", record.size());
}

int main() {
    read();
    work();
    return 0;
}

合唱

Description

小Q和牛博士合唱一首歌曲,这首歌曲由n个音调组成,每个音调由一个正整数表示。
对于每个音调要么由小Q演唱要么由牛博士演唱,对于一系列音调演唱的难度等于所有相邻音调变化幅度之和, 例如一个音调序列是8, 8, 13, 12, 那么它的难度等于|8 - 8| + |13 - 8| + |12 - 13| = 6(其中||表示绝对值)。
现在要对把这n个音调分配给小Q或牛博士,让他们演唱的难度之和最小,请你算算最小的难度和是多少。
如样例所示: 小Q选择演唱{5, 6}难度为1, 牛博士选择演唱{1, 2, 1}难度为2,难度之和为3,这一个是最小难度和的方案了。

Input

输入包括两行,第一行一个正整数n(1 ≤ n ≤ 2000) 第二行n个整数vi, 表示每个音调。

Output

输出一个整数,表示小Q和牛博士演唱最小的难度和是多少。

Sample Input

5
1 5 6 2 1

Sample Output

3

解题思路

正推的话,容易想到一个人继续演唱或换人演唱的时候发生状态转移:

  • 设 dp[i][j] 表示当前小Q唱到第 i 个音调,牛博士唱到第 j 个音调的难度和;
  • 不妨设当前 i > j :
    • 若 i - 1 == j 则发生换人,由于不知道上一次 i 唱到哪里,状态由 min{ dp[k][j] + abs(v[i] - v[k]) }, k < j 转移来;
    • 若 i - 1 > j 则表示当前是从 i - 1 唱到 i 的,没有换人,状态由 dp[i-1][j] + abs(v[i] - v[i-1]) 累加;
  • 不妨设 dp[i][j] 表示当前演唱到第 i 个,上一个人演唱到第 j 个,则状态转移方程为
    dp[i][j] = dp[i-1][j] + abs(v[i] - v[i-1]), j < i - 1
    dp[i][i -1] = min{ dp[i-1][k] + abs(v[i] - v[k]) }, k < i - 1
  • 边界情况是一个人唱第一个,后面所有音调让另一个人唱
    dp[i][0] = dp[i-1][0] + abs(v[i] - v[i-1]), i ≥ 2
    或者一个人唱前面所有音调,最后一个音调让另一个人唱
    dp[i][i-1] = dp[i-1][i-2] + abs(v[i-1] - v[i-2]), i ≥ 2

反推:

  • 设 dp[i][j] 表示从当前小Q唱到第 i + 1 个音调,牛博士唱到第 j + 1 个音调开始,直到所有音调演唱完的难度和;边界情况 i = 0 或 j = 0 表示一个人还没开始唱;
  • 容易知道下一个音调 next = max{ i, j } ,若让小Q唱下一个音调,会得到 abs(v[next] - v[i]) 的贡献;若让牛博士唱会得到 abs(v[next] - v[j]) 的贡献;
  • 状态转移方程
    dp[i][j] = min{ dp[next][j] + abs(v[next] - v[i]), dp[i][next] + abs(v[next] - v[j]) }, i ≠ j < n

AC 代码

正推:

#include <stdio.h>
#include <stdlib.h>

typedef long long llong;

inline void getMin(llong& n, llong x) {
    n > x && (n = x);
}

#define MAXN 2020

int n;
int v[MAXN], cost[MAXN];

void read() {
    scanf("%d%d", &n, v);
    for (int i = 1; i < n; ++i) {
        scanf("%d", v + i);
        cost[i] = abs(v[i] - v[i - 1]);
    }
}

llong dp[MAXN][MAXN];

void work() {
    llong res = (1ll << 63) - 1;
    for (int i = 2; i < n; ++i) {
    //    dp[i][0] = dp[i - 1][0] + cost[i];
        dp[i][i - 1] = dp[i - 1][i - 2] + cost[i - 1];
    }
    for (int i = 2; i < n; ++i) {
        for (int j = 0; j < i - 1; ++j) {
            dp[i][j] = dp[i - 1][j] + cost[i];
            getMin(dp[i][i - 1], dp[i - 1][j] + abs(v[i] - v[j]));
        }
    }
    for (int i = 0; i < n - 1; ++i) {
        getMin(res, dp[n - 1][i]);
    }
    printf("%lld\n", res);
}

int main() {
    read();
    work();
    return 0;
}

反推要做一些变动:

using std::max;
using std::min;

int n, v[MAXN];

void read() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        scanf("%d", v + i);
    }
}

int cost(int a, int b) {
    return a && b ? abs(v[a] - v[b]) : 0;
}

记忆化递归搜索:

llong solve(int i, int j) {
    int next = max(i, j) + 1;
    if (next == n + 1) {
        return 0;
    }
    if (!~dp[i][j]) {
        dp[i][j] = min(solve(next, j) + cost(next, i), solve(i, next) + cost(next, j));
    }
    return dp[i][j];
}

void work() {
    memset(dp, 0xff, sizeof(dp));
    llong res = solve(0, 0);
    printf("%lld\n", res);
}

用循环更简单:

void work() {
    for (int i = n - 1; i >= 0; --i) {
        for (int j = n - 1; j >= 0; --j) {
            int next = max(i, j) + 1;
            dp[i][j] = min(dp[next][j] + cost(next, i), dp[i][next] + cost(next, j));
        }
    }
    printf("%lld\n", dp[0][0]);
}



本文基于 知识共享许可协议知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 发布,欢迎引用、转载或演绎,但是必须保留本文的署名 BlackStorm 以及本文链接 http://www.cnblogs.com/BlackStorm/p/7499974.html ,且未经许可不能用于商业目的。如有疑问或授权协商请 与我联系

posted @ 2017-09-10 07:40  BlackStorm  阅读(...)  评论(... 编辑 收藏