【题目自解】北京大学2018计算机学科夏令营上机考试
【题目自解】北京大学2018计算机学科夏令营上机考试
A:计算两个日期之间的天数(NOI 1.13编程基础之综合应用)
解题思路
简单题,重点在闰年的判断和闰月的设置。
AC代码
#include<cstdio> bool isLeapYear(int x) { return (x % 4 == 0 && x % 100 != 0) || x % 400 == 0; } int month[13][2] = {0,0,31,31,28,29,31,31,30,30,31,31,30,30,31,31,31,31,30,30,31,31,30,30,31,31}; int main() { int sy, sm, sd; scanf("%d%d%d", &sy, &sm, &sd); int ey, em, ed; scanf("%d%d%d", &ey, &em, &ed); int cnt = 0; while (sy != ey || sm != em || sd != ed) { sd++; cnt++; if (sd > month[sm][isLeapYear(sy)]) { sd = 1; sm++; if (sm > 12) { sm = 1; sy++; } } } printf("%d\n", cnt); return 0; }
B:回文子串(NOI 1.7编程基础之字符串)
解题思路
简单题。从长度为2的回文子串开始思考,重点是回文子串的while循环判断。
AC代码
#include<iostream> #include<cstring> using namespace std; int main() { char s[505]; cin >> s; int len = strlen(s); for (int j = 1; j < len; j++)//回文子串长度-1 { for (int i = 0; i < len - j; i++) { int beg = i; int end = i + j; bool flag = false;//是否是回文子串 while (s[beg] == s[end]) { beg++; end--; if (end <= beg) flag = true; } if (flag) { for (int m = i; m <= i + j; m++)cout << s[m]; cout << endl; } } } return 0; }
E:重要逆序对
解题思路
AC代码
#include<cstdio> const int N = 200500; int a[N]; int t[N]; long long ans = 0; void merge(int l, int m, int r) { int k = l, i = l, j = m + 1; while (i <= m && j <= r) { if (a[i] <= 2 * a[j])i++; else { j++; ans += m - i + 1; } } k = l, i = l, j = m + 1; while (i <= m && j <= r) { if (a[i] > a[j]) t[k++] = a[j++]; else t[k++] = a[i++]; } while (i <= m)t[k++] = a[i++]; while (j <= r)t[k++] = a[j++]; for (int i = l; i <= r; i++)a[i] = t[i]; } void mergeSort(int l, int r) { int m = (l + r) / 2; if (l < r) { mergeSort(l, m); mergeSort(m + 1, r); merge(l, m, r); } } int main() { int n; scanf("%d", &n); for (int i = 0; i < n; i++)scanf("%d", &a[i]); mergeSort(0, n-1); printf("%lld", ans); return 0; }
G:食物链
解题思路
并查集的经典题。
重点在合并过程中的关系域更新,在第一篇参考博客中向量法的讲解很清晰。
AC代码
#include<cstdio> const int maxn = 50000 + 10; int p[maxn]; //存父节点 int r[maxn];//存与父节点的关系, 0同一类,1被父节点吃,2吃父节点 void set(int n) //初始化 { for (int x = 1; x <= n; x++) { p[x] = x; //开始自己是自己的父亲节点 r[x] = 0;//开始自己就是自己的父亲,每一个点均独立 } } int find(int x) //找父亲节点 { if (x == p[x]) return x; int t = p[x]; p[x] = find(p[x]); r[x] = (r[x] + r[t]) % 3; //回溯由子节点与父节点的关系和父节点与根节点的关系找子节点与根节点的关系 return p[x]; } void Union(int x, int y, int d) { int fx = find(x); int fy = find(y); p[fy] = fx; //合并树 注意:被 x 吃,所以以 x 的根为父 r[fy] = (r[x] - r[y] + 3 + (d - 1)) % 3; //对应更新与父节点的关系 } int main() { int n, m; scanf("%d%d", &n, &m); set(n); int ans = 0; int d, x, y; while (m--) { scanf("%d%d%d", &d, &x, &y); if (x > n || y > n || (d == 2 && x == y)) ans++; //如果节点编号大于最大编号,或者自己吃自己,说谎 else if (find(x) == find(y)) //如果原来有关系,也就是在同一棵树中,那么直接判断是否说谎 { if (d == 1 && r[x] != r[y]) ans++; //如果 x 和 y 不属于同一类 if (d == 2 && (r[x] + 1) % 3 != r[y]) ans++; // 如果 x 没有吃 y (注意要对应Uinon(x, y)的情况,否则一路WA到死啊!!!) } else Union(x, y, d); //如果开始没有关系,则建立关系 } printf("%d\n", ans); return 0; }
F:Tram
解题思路
就是最基础的最短路问题,在邻接矩阵的构建过程当中,不需要旋转的路径设置为0,需要旋转的路径设置为1,不可达的路径为INF。之后利用任意一种最短路径方法便可以求得结果。本题明显用Floyd更简单,虽然粗暴,但是代码量少啊。
注意: d[u]有一个相加操作,所以在设置很大整数时不能设置为0x7fffffff,会导致溢出,可以设置为0x3fffffff。
AC代码(Dijkstra算法)
#include <cstdio> using namespace std; const int N = 110; const int INF = 0x3fffffff; int G[N][N]; // 图邻接矩阵 int d[N]; // 表示起点到各结点的最短距离 bool vis[N] = { false }; // 表示各结点被访问过与否 int n, start_, end_; void init() // 初始化 { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (i == j) G[i][j] = 0; else G[i][j] = INF; } } } void Dijkstra(int start) { for (int i = 1; i <= n; i++) d[i] = INF;//可达距离都初始化为无穷 d[start] = 0; // 起初只有start结点可达 for (int i = 1; i <= n; i++) { int u = -1; // 距离最近的结点u,初始化为-1 int min = INF; // min存放最小的d[u],初始化为无穷 for (int j = 1; j <= n; j++)//得到最近的点 { if (vis[j] == false && d[j] < min) { u = j; // 与起点直接相连的距离最小结点 min = d[j]; } } if (u == -1) return; // 说明剩下的未被访问过的结点都是与起点不可达的 vis[u] = true; // 设为已读 for (int v = 1; v <= n; v++)//由新得到的点更新后面所有点 { if (vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]) d[v] = d[u] + G[u][v]; } } } int main() { scanf("%d %d %d", &n, &start_, &end_); init(); // 初始化 for (int i = 1; i <= n; i++) { int k; scanf("%d", &k); for (int j = 0; j < k; j++) { int t; scanf("%d", &t); if (j == 0) G[i][t] = 0; // 第一个连接点不需要旋转 else G[i][t] = 1; // 之后的每一次都需要旋转1次 } } Dijkstra(start_); if (d[end_] == INF) printf("%d", -1); else printf("%d", d[end_]); return 0; }
AC代码(Floyd算法,此时结点不超过200,可以用)
#include <cstdio> using namespace std; const int N = 110; const int INF = 0x3fffffff; int G[N][N]; // 图邻接矩阵 int n, start_, end_; void init() // 初始化 { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (i == j) G[i][j] = 0; else G[i][j] = INF; } } } void Floyd() { for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (G[i][k] == INF || G[k][j] == INF)continue; else if (G[i][j] == INF || G[i][j] > G[i][k] + G[k][j])G[i][j] = G[i][k] + G[k][j]; } } } } int main() { scanf("%d %d %d", &n, &start_, &end_); init(); // 初始化 for (int i = 1; i <= n; i++) { int k; scanf("%d", &k); for (int j = 0; j < k; j++) { int t; scanf("%d", &t); if (j == 0) G[i][t] = 0; // 第一个连接点不需要旋转 else G[i][t] = 1; // 之后的每一次都需要旋转1次 } } Floyd(); if (G[start_][end_] == INF) printf("%d", -1); else printf("%d", G[start_][end_]); return 0; }
C:The Die Is Cast
解题思路
简单题,双层DFS,没人做的原因很大程度是因为英文题太tm长了。
AC代码
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; int w, h, num[1000], l; int dx[4] = { 1,0,-1,0 }; int dy[4] = { 0,-1,0,1 }; char map[100][100]; int vis[100][100]; void ddfs(int s, int e) { int i, j; map[s][e] = \'*\'; for (i = 0; i < 4; i++) { int xx = s + dx[i]; int yy = e + dy[i]; if (xx >= 0 && xx < h&&yy >= 0 && yy < w&&map[xx][yy] == \'X\') ddfs(xx, yy); } } void dfs(int s, int e) { int i, j; map[s][e] = \'.\'; for (i = 0; i < 4; i++) { int xx = s + dx[i]; int yy = e + dy[i]; if (xx >= h || yy >= w || xx < 0 || yy < 0 || map[xx][yy] == \'.\') continue; if (map[xx][yy] == \'X\') { ddfs(xx, yy); num[l]++; } if (map[xx][yy] == \'*\') dfs(xx, yy); } } int main() { int i, j, k = 1; while (~scanf("%d%d", &w, &h)) { if (!w && !h)break; memset(map, 0, sizeof(map)); memset(num, 0, sizeof(num)); l = 0; for (i = 0; i < h; i++) scanf("%s", map[i]); for (i = 0; i < h; i++) { for (j = 0; j < w; j++) { if (map[i][j] == \'*\') { dfs(i, j); l++; } } } printf("Throw %d\n", k++); sort(num, num + l); for (i = 0; i < l - 1; i++) { printf("%d ", num[i]); } printf("%d", num[i]); printf("\n\n"); } return 0; }
D:Euro Efficiency
解题思路
两次完全背包。由于构造一个面值的金钱需要加减货币价值, 所以对于如下构造:100=55+55-4-4-2 我们可以定义55+55作为最大付款数, -4-4-2为找零数,100是最终付款数。我们把每个上面的构造过程分成两部分: 构造最大付款数和找零.
第一步构造最大付款数: (一次完全背包过程)
- 状态:dp[i][j]==x 表示用前i种货币相加能得到的面值为j的金钱时, 最少需要x个货币.
- 初始值dp全为INF(无穷大), dp[0][0]=0.
- 状态转移方程: dp[i][j] = min( dp[i-1][j] , dp[i][j-val[i]]+1 ),min的前项表示第i种货币一个都不用, 后项表示用1个第i种货币.
最终dp[n][0]到dp[n][maxn]都是我们需要的数据. 它们表示当我们只加不减的时候最少需要多少个货币能达到金钱面值.
第二步找零: (另一次完全背包过程)
- 状态:现在考虑是否能通过找零进一步减小达到特定面值需要的货币数,比如58可以用50+5+2+1达到,但是用60-2更容易达到。dp[i][j]==x 表示当决策完前i个物品后(如果选第i种物品,就表示在当前货币值的基础上减去val[i])具有金钱面值为j时, 最少需要x个货币。
- 初始值: 上一轮DP的结果.
- 状态转移方程: dp[i][j-val[i]] = min( dp[i-1][j-val[i]] , dp[i][j]+1),前项表示第i种货币一个都不减, 后项表示至少减去1个第i种货币.
最终dp[n][1]到dp[n][100]都是我们需要的数据。程序实现使用优化后的一维dp。另外,第二步找零要用反向背包,因为dp[j] 是从下标值大于 j 的状态转移而来,故更新方向需和整数重量值的时候相反。
AC代码
#include<cstring> #include<cstdio> #include<algorithm> const int N = 2005; const int INF = 0x3fffffff; using namespace std; int val[7];//货币面值 int dp[N]; int n = 7;//货币种类数 int init() { for (int i = 0; i < N; i++) dp[i] = INF; dp[0] = 0; } void package()//正向DP,更新 dp[最大付款数] { for (int i = 1; i <= n; i++) { for (int j = val[i]; j < N; j++) dp[j] = min(dp[j], dp[j - val[i]] + 1); } } void repackage() { for (int i = 1; i <= n; i++) { for (int j = N - 1; j >= val[i]; j--) dp[j - val[i]] = min(dp[j - val[i]], dp[j] + 1); } } int main() { int T; scanf("%d", &T); while (T--) { for (int i = 1; i <= 6; i++) scanf("%d", &val[i]); init(); package(); repackage(); int sum = 0, max_val = 0;//统计结果输出 for (int i = 1; i <= 100; i++) { sum += dp[i]; max_val = max(max_val, dp[i]); } printf("%.2f %d\n", sum / 100.0, max_val); } }