牛客小白月赛21--题解报告

牛客小白月赛21–题解报告

时间:2020.01.19
个人博客:https://wyjoutstanding.github.io/
第一次参加算法比赛,记录一下


文章目录

  • 牛客小白月赛21--题解报告
    • A. Audio
      • 思路分析
      • AC代码(C++11,手算xy)
    • B. Bits
      • 思路分析
      • AC代码(C++11,汉诺塔递归设计)
    • C. Channels
      • 思路分析
      • AC代码(C++11,固定起点)
    • D. DDoS
      • 思路分析
      • AC代码(C++11,拓扑排序,dp计数)
    • E. Exams
      • 思路分析
      • AC代码(C++11)
    • F. Fool Problem
      • 思路分析
      • AC代码(C++11,找规律)
    • G. Game
      • 思路分析
      • AC代码(C++11,质因数分解)
    • H. "Happy New Yaer!"
      • AC代码(C++11)
    • I. I love you
      • 思路分析
      • AC代码(C++,子序列计数+dp滚动数组)
    • J. Jelly
      • 思路分析
      • AC代码(C++11,三维迷宫+BFS)
    • 总结

题号 考点 备注
A 外心坐标计算 手算/线性方程
B 哈诺塔递归设计 手动调试
C 前缀和/线性化 固定起点(日期计算)
D 拓扑排序,通路计数 简单dp,bfs
E 简单模拟 注意四舍五入
F 找规律,大整数 手动猜想/通式带入化简
G 质因数分解 仅需计算质因数个数
H 直接输出 签到题
I 子序列计数dp 包括连续和非连续,滚动数组优化(逆序遍历)
J 三维迷宫 BFS搜索6个方向

难度排序:

  • 签到:H
  • 常规:A,C,E,J(简单模拟)
  • 思考:F,G(规律找到后巨简单)
  • 较难:B,D,I(设计dp,图论,递归搜索)

A. Audio

思路分析

已知三点坐标,求解外心坐标(中垂线交点)。

  • 可直接用两点式写出直线方程,联立求解二元一次方程,手算出(x,y)结果。

  • 或者用高斯消元求解线性方程。

AC代码(C++11,手算xy)

#include
using namespace std;
int main() {
    double p[3][2], m[2][2], A[2], B[2]; // 三点坐标,中点坐标,中垂线法向量
    for (int i = 0; i < 3; i ++) scanf("%lf %lf", &p[i][0], &p[i][1]);
    for (int i = 0; i < 2; i ++) {
        m[i][0] = (p[i][0] + p[i+1][0]) / 2; // x
        m[i][1] = (p[i][1] + p[i+1][1]) / 2; // y
        A[i] = p[i][0] - p[i+1][0]; // 法向量x
        B[i] = p[i][1] - p[i+1][1]; // 法向量y
    }
    double x, y;
    x = -(-B[1]*A[0]*m[0][0] - B[0]*B[1]*m[0][1] + B[0]*B[1]*m[1][1] + B[0]*A[1]*m[1][0]) / (B[1]*A[0] - B[0]*A[1]);
    y = m[0][1] - A[0]*(x-m[0][0])/B[0];
    printf("%.3lf %.3lf\n", x, y);
    return 0;
}

B. Bits

思路分析

极有意思的一道题,深入理解了汉诺塔递归抽象原理。

只需在原来的汉诺塔基础上,控制奇偶放置位置即可。

递归设计比较简单,建议手动演示推导。

void hanoi(int n, vector<int>& A, vector<int>& B, vector<int>& C) { // 递归实现:表示将A柱上n个盘子借助B移动到C
    if (n == 1) { // 1个直接移动
        move(A, C);
        return;
    }
    hanoi(n-1, A, C, B); // A柱上n-1个盘子借助C移动到B
    move(A, C);
    hanoi(n-1, B, A, C);
}

AC代码(C++11,汉诺塔递归设计)

#include
using namespace std;
int n, cnt = 0, p[3]; // 总个数,移动次数,三根柱子的位置
vector<int> V[3]; // 3个柱子
char a[20][100]; // 存放输出
void showRst() { // 打印结果
    int N = 3*(2*n+1) + 4; // 列宽
    for (int i = 0; i < n + 3; i ++) { // 初始化
        for (int j = 0; j < N; j ++) {
            if (i == n + 2) a[i][j] = '-';
            else {
                if (i != 0 && (p[0] == j || p[1] == j || p[2] == j)) a[i][j] = '|';
                else a[i][j] = '.';
            }
        }
    }
    for (int i = 0; i < 3; i ++) { // 三个柱子对应的盘子打印
        int j = n+1; // 从n+1逆向打印
        for (auto k : V[i]) {
            for (int k2 = p[i] - k; k2 <= p[i] + k; k2 ++) a[j][k2] = '*'; // 盘子宽度:2k+1
            j --;
        }
    }
    ++ cnt; // 统计移动次数
    for (int i = 0; i < n+3; i ++) { // 打印最终结果
        if ((cnt == 1 << n) && i == n + 2) break; // 排除最后一次移动的分割线
        for (int j = 0; j < N; j ++) printf("%c", a[i][j]);
        printf("\n");
    }
}
void move(vector<int>& A, vector<int>& B) { // 将A的最后一个元素移到B的最后一个元素后
    B.push_back(A.back());
    A.pop_back();
    showRst(); // 移动就打印结果
}
void hanoi(int n, vector<int>& A, vector<int>& B, vector<int>& C) { // 递归实现:表示将A柱上n个盘子借助B移动到C
    if (n == 1) { // 1个直接移动
        move(A, C);
        return;
    }
    hanoi(n-1, A, C, B); // A柱上n-1个盘子借助C移动到B
    move(A, C);
    hanoi(n-1, B, A, C);
}
int main() {
    scanf("%d", &n);
    for (int i = n; i > 0; i --) V[0].push_back(i); // 初始化
    for (int i = 0; i < 3; i ++) {
        p[i] = (2*n+2)*i + 1 + n; // 每个柱子的位置
    }
    showRst(); // 原始打印
    if (n % 2 == 0) hanoi(n, V[0], V[1], V[2]); // 根据奇偶控制转移到的位置
    else hanoi(n, V[0], V[2], V[1]);
    return 0;
}

C. Channels

思路分析

类似于时间段求解(前缀和),可固定起点1,分别计算出1(t1-1)和1t2的时间,做差即为结果。

对于时间1~t的求解,除法和求余即可。

AC代码(C++11,固定起点)

#include
using namespace std;
long long getTime(long long t) {
    long long mod = t % 60;
    return (t / 60) * 50 + ((mod <= 50) ? mod : 50);
}
int main() {
    long long t1, t2;
    while(scanf("%lld %lld", &t1, &t2) == 2) {
        printf("%lld\n", getTime(t2) - getTime(t1-1));
    }
    return 0;
}

D. DDoS

思路分析

题目表意不清,说得天花乱坠,其实就是给定一个有向无环图,找出结点1~n的路径条数。

这里的最优策略指得是同一时间接受请求越多,则攻击越成功,因为可以通过调整每条通路开始时间,使到达n的所有请求时刻一致。

因此,在拓扑排序过程中即可完成路径数计算,其中dp[i]表示从1到大i的路径数目,对于单向边u->v,当u的入度为0时,dp[v]+=dp[u]

注意取模,且需要考虑重边问题,若用邻接表存储则无需考虑

测试样例

//输入
5 8
1 2 3
1 3 1
2 5 1
3 5 3
1 2 3
1 4 1
2 4 1
4 5 1
//输出
6

AC代码(C++11,拓扑排序,dp计数)

#include
using namespace std;
#define MAXN 100010
#define MOD 20010905
vector<int> adj[MAXN], dp(MAXN, 0), indegree(MAXN, 0); // 邻接表,dp[i]表示达到节点i的路径数,入度
int main() {
    int n, m, u, v, w;
    scanf("%d %d", &n, &m);
    for (int i = 0; i < m; i ++) {
        scanf("%d %d %d", &u, &v, &w);
        adj[u].push_back(v);
        indegree[v] ++; // 入度计算
    }
    // 拓扑排序过程计算条数
    queue<int> q;
    for (int i = 1; i <= n; i ++) { // 将第一批入度为0顶点入队
        if (indegree[i] == 0) {
            dp[i] = 1;
            q.push(i);
        }
    }
    while(!q.empty()) {
        u = q.front(); q.pop();
        for (auto i : adj[u]) {
            dp[i] = (dp[i] + dp[u]) % MOD; // 计算转移到下一点的路径数
            indegree[i] --; // 入度扣除
            if (indegree[i] == 0) q.push(i); // 入度为0则入队
        }
    }
    printf("%d\n", dp[n]);
    return 0;
}

E. Exams

思路分析

根据题目规定计算分数即可,注意以下几点:

  • 课程性质为任选(编号2)的不计算

  • 每科平时分,期中,期末分数乘以相应比例再求和后,需要四舍五入取整

  • 最终结果需四舍五入(保留2位小数,即求解到千分位)

AC代码(C++11)

#include
using namespace std;
int main() {
    double credit, tot=0.0, s, p, sum1=0.0, sum2=0.0;
    int n, t;
    scanf("%d", &n);
    while(n --) {
        tot = 0.0;
        scanf("%d %lf", &t, &credit);
        for (int i = 0; i < 3; i ++) {
            scanf("%lf %lf", &s, &p);
            tot += s * p;
        }
        if (t != 2) { // 忽略任选科目
            sum1 += credit; // 学分累加
            sum2 += (int)(tot+0.5) * credit; // 四舍五入
        }
    }
    // 处理保留两位小数,根据千分位进行四舍五入
    double ans = sum2 / sum1;
    ans = (int)(ans*100+0.5) / 100.0;
    printf("%.2lf\n", ans);
    return 0;
}

F. Fool Problem

思路分析

斐波那契数列翻新,两种思路,容易发现fn=(-1)^n

  • 直接找规律,列举4个差不多可以发现。
  • 将fn的通项式带入表达式,化简可得到结果。

对于输入值,乍一看需大整数处理,实则不用,只需用字符串读入,判断最后一位的奇偶性即可。

AC代码(C++11,找规律)

#include
using namespace std;
int main() {
    string s;
    cin >>s;
    printf("%d\n", ((s.back() - '0') % 2 == 0) ? 1 : -1);
    return 0;
}

G. Game

思路分析

可操作次数,就是n的质因数个数-1。比如8=2*2*2,3个质因数,可进行两次分解操作,再如40=2*2*2*5,4个质因数,3次分解操作。

除此之外,注意n=1时为特例,0次分解操作。

AC代码(C++11,质因数分解)

#include
using namespace std;
#define N 400
int main() {
    int n, cnt = 0;
    scanf("%d", &n);
    for (int i = 2; i < N && n != 1; i ++) { // 统计能分解的质因数个数
        while(n % i == 0 && n != i) { // 不包含最后一个自身
            cnt += 1; // 累计次数
            n /= i; // 下一个数
        }
    }
    printf("%s\n", ((cnt % 2) == 0) ? "Nancy" : "Johnson");
    return 0;
}

H. “Happy New Yaer!”

签到题,输出题目即可

AC代码(C++11)

#include
using namespace std;
int main() {
    printf("\"Happy New Year!\"");
    return 0;
}

I. I love you

思路分析

子序列计数,简单dp加滚动数组优化。

dp[i]表示在当前长度下,满足S[1…i]序列的方案数,根据下一个字符ch及ch的前一个字符计算个数。

若用滚动数组优化,最好逆序求解,避免存在相同的字母照成感染,如aa这种情况(不过这题不存在这种情况)

AC代码(C++,子序列计数+dp滚动数组)

#include
using namespace std;
#define MOD 20010905
int dp[9] = {1,0}; // dp[0]=1,为第一个字符做准备
int main() {
    string s, str = "tiloveyou";
    cin >> s;
    for (auto ch : s) {
        for (int i = 8; i >= 1; i --) { // 逆序遍历,避免相邻相同的字符相互感染
            dp[i] = (dp[i] + (str[i] == tolower(ch)) * dp[i-1]) % MOD; // DP滚动数组优化
        }
    }
    printf("%d\n", dp[8]);
    return 0;
}

J. Jelly

思路分析

二维迷宫进阶,三维迷宫用bfs求解。

只需在二维迷宫遍历前后左右四个方向基础上,增加上下两个方向即可。

AC代码(C++11,三维迷宫+BFS)

#include
using namespace std;
#define N 101;
int maze[101][101][101] = {0}; // 存储迷宫地图,1表示不通
int dict[6][3] = {{0,0,1}, {0,0,-1}, {0,1,0}, {1,0,0}, {0,-1,0}, {-1,0,0}}; // 控制前后左右上下六个方向(x,y,z)
struct Pos {
    int x, y, z, d; // x,y,z坐标和层次
    Pos(int _x, int _y, int _z, int _d): x(_x), y(_y), z(_z), d(_d){}
    Pos(){}
}pos;
int n, x, y, z, ans = -1;
int main() {
    char t;
    scanf("%d", &n);
    for (int i = 0; i < n; i ++) {
        for (int j = 0; j < n; j ++) {
            for (int k = 0; k < n; k ++) {
                scanf(" %c", &t); // 空格用来吸收空格和换行
                if (t != '.') maze[i][j][k] = 1; // 不可走
            }
        }
    }
    queue<Pos> q;
    q.push(Pos(0,0,0,1));
    while(!q.empty()) { // BFS
        pos = q.front();
        q.pop();
        if (pos.x == n - 1 && pos.y == n-1 && pos.z == n-1) { // 终点
            ans = pos.d;
            break;
        }
        for (int i = 0; i < 6; i ++) { // 6个方向
            x = pos.x + dict[i][0]; y = pos.y + dict[i][1]; z = pos.z + dict[i][2];
            if (x >= 0 && x < n && y >= 0 && y < n && z >= 0 && z < n // 范围合法
            && maze[x][y][z] == 0) { // 未访问
                maze[x][y][z] = 1; // 置为已访问
                q.push(Pos(x,y,z,pos.d+1));
            }
        }
    }
    printf("%d\n", ans);
    return 0;
}

总结

说是小白月赛,第一次参加算法比赛,只做出4道题,汗颜,因此,用了两天时间将不会的题目补了出来,其实真不难,只是第一次遇见不知道咋做。

这个题目出得不太严谨,测试用例规模描述太少,题目名称为了和序号一致显得牵强附会

不过通过比赛这种形式可以提高学习效率,查看他人AC代码学习到了很多。

写算法确实对许多问题有了更深刻理解,比如汉诺塔,大一C语言就做过,这里重新来了一遍,整个过程明了不少,同时还学会了如何求解线性方程组与一些基础的数论知识。

你可能感兴趣的:(算法)