【题解】2020牛客寒假算法基础集训营第六场
写在题解前:
这次比赛的10道题,std的代码量都非常小,但是需要一点点思维来做保障。
相信能活跃大家的思路,以及优化大家的切题体验。(认真.jpg)
以及即使场上不会做,补完这套题还是会给大家收获的~
槽点举例:
Q:J我怎么随便写就过了?
A:这题真的怎么写都是对的,即使你没想清楚所有细节。(滑稽)
Q:怎么B题就是个Tarjan模板题?
A:我不是我没有,强行用Tarjan明显要比std难很多嘛。
A 配对
我们要使得第K大的和尽可能大,显然可以贪心:
首先,组成这K对数字的显然是A中最大的K个数字和B中最大的K个数字。
问题转化为怎样配对使得最小的和最大:
我们发现,如果A1<A2,B1<B2,那么一定是由A1和B2配对较优。
经过简单的归纳可以得到,倒序配对是最优的,这样就解决了问题。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; const int N = 100050; int a[N], b[N], n, k, ans = 2e8; int main() { int i, j; scanf("%d%d", &n, &k); for(i = 1; i <= n; i ++) scanf("%d", &a[i]); for(i = 1; i <= n; i ++) scanf("%d", &b[i]); sort(a + 1, a + n + 1); sort(b + 1, b + n + 1); for(i = n; i > n - k; i --) ans = min(ans, a[i] + b[n - k + n + 1 - i]); printf("%d\n", ans); return 0; }
B 图
这是一个简单的有关基环树的问题。
可以证明,每个点出度都为1的有向图是一个基环内向树森林。
关于这一部分的内容,可以自行查阅资料。
这里我们要用到的结论是:从一个点出发,沿着出边一路走下去,一定会走到一个环。
所以我们选择dfs,当遍历到一个已在dfs栈中的节点时,就说明找到了环,可以结束统计。
但这样是会超时的,于是我们选择带“记忆化”的dfs,从一个点开始沿着出边走下去,每当走到一个在之前某次dfs中经过的点时,就可以利用上一次的答案完成求解。
实现上有一些细节,要注意不要让复杂度退化。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; const int N = 1000050; int to[N], siz[N], n, vis[N], ins[N], sta[N]; int dfs(int x){ if(siz[x]) return siz[x]; return siz[x] = 1 + dfs(to[x]); } int main() { int i, j, k, h; scanf("%d", &n); for(i = 1; i <= n; i ++) scanf("%d", &to[i]); for(i = 1; i <= n; i ++){ if(vis[i] == 0){ j = i; while(vis[j] == 0){ sta[++ sta[0]] = j; vis[j] = ins[j] = 1; j = to[j]; } if(ins[j]){ k = j, h = 0; do{ k = to[k]; h ++; }while(k != j); do{ k = to[k]; siz[k] = h; }while(k != j); } while(sta[0]){ ins[sta[sta[0]]] = 0; sta[0] --; } } } for(i = 1, h = 0; i <= n; i ++) h = max(h, dfs(i)); printf("%d", h); return 0; }
C 汉诺塔
将木板按照Xi从小到大排序,将这时的Yi数列记为Zi数列,则问题变成将Zi划分为尽可能少的若干组上升子序列。
根据Dilworth定理,最小组数等于Zi的最长下降子序列长度。
要求最长下降子序列的长度,我们有一种经典的二分优化dp的方法,在这里不再详述。 借助这种做法我们能给出一种构造方法,在求出最小组数的同时得出方案。
将状态数组的每个位置变为栈,用入栈操作代替修改元素操作,即可在求出组数的同时,用这些栈来完成对数列的划分。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; const int N = 100050; struct node{ int x, y, loc; inline bool operator < (const node &b)const{ return x < b.x; } }a[N]; int n, m, ot[N], S[N]; int main() { int i, j, k; scanf("%d", &n); for(i = 1; i <= n; i ++){ scanf("%d%d", &a[i].x, &a[i].y); a[i].loc = i; } sort(a + 1, a + n + 1); for(i = 1; i <= n; i ++){ int lb = 1, rb = m; while(lb <= rb){ int md = lb + rb >> 1; if(S[md] < a[i].y) rb = md - 1; else lb = md + 1; } S[lb] = a[i].y; if(lb > m) m = lb; ot[a[i].loc] = lb; } printf("%d\n", m); for(i = 1; i <= n; i ++) printf("%d ", ot[i]); return 0; }
D 重排列
容易知道按升序将A和B排序不影响结果。
按标号从小到大考虑A的每个位置填什么数。
例:A(1,2,3);B(1,3,4)
则考虑第一个位置时,只能填1。
考虑第二个位置时,可以填2或3。
但是由于2和3在这里是完全等价的,也就是说我们并不关心填了谁。
那么我们只需要记录每一步有多少个数可填就好了,这个答案与之前填入的方案无关。
具体实现的时候只需要用双指针进行一轮扫描就可以了。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; typedef long long LL; const int N = 100050; const LL mod = 1000000007; int a[N], b[N], ans = 1, n; int main() { int i, j, k; cin >> n; for(i = 1; i <= n; i ++) scanf("%d", &a[i]); for(i = 1; i <= n; i ++) scanf("%d", &b[i]); sort(a + 1, a + n + 1); sort(b + 1, b + n + 1); for(i = 1, j = 0; i <= n; i ++){ while(j < n && a[j + 1] <= b[i]) j ++; ans = (LL)ans * max(0ll, j - i + 1ll) % mod; } printf("%d", (ans + mod) % mod); return 0; }
E 立方数
考虑直接一些的做法
尝试对每个N作质因数分解,经简单的统计可得出答案,复杂度O(TN^(1/2))
我们先做简单一点的优化,容易发现其实只要枚举10^6(N^(1/3)以内)的质数就好,复杂度O(TN^(1/3)/ln(N^(1/3)))
再作进一步的分析,如果我们仅使用N^(1/4)(记为W)以内的质数去试除,那么最后余下的数X仅具有大于W的因子
此时X要么是一个完全立方数,要么对答案没有任何贡献,只需要使用二分法来验证X是不是一个完全立方数即可
复杂度O(TN^(1/4)/ln(N^(1/4))),不卡常数,真的!
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <time.h> using namespace std; typedef long long LL; const int N = 31700; int is[N], n, pri[N]; LL x, p_tri[N], p_ss[N]; int main() { int i, j, k; for(i = 2; i < N; i ++){ if(is[i] == 0){ pri[++ pri[0]] = i; p_tri[pri[0]] = (LL)i * i * i; p_ss[pri[0]] = (LL)i * i * i * i; for(j = i * i; j < N; j += i) is[j] = 1; } } scanf("%d", &n); while(n --){ int ans = 1; scanf("%lld", &x); for(i = 1; p_ss[i] <= x; i ++){ if(x % pri[i] == 0){ while(x % p_tri[i] == 0){ ans *= pri[i]; x /= p_tri[i]; } while(x % pri[i] == 0) x /= pri[i]; } } int lb = 1, rb = 1000000; while(lb <= rb){ int md = lb + rb >> 1; if((LL)md * md * md < x) lb = md + 1; else rb = md - 1; } if((LL)lb * lb * lb == x) ans *= lb; printf("%d\n", ans); } return 0; }
F 十字阵列
设3个数组a[],b[]和ab[][],分别统计对行,列和每格造成的伤害
进行操作(xi,yi,zi)时,a[xi],b[yi],ab[xi][yi]都加上zi
每次询问一格的答案时,只要查询a[x]+b[y]-ab[x][y]就好了
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; const int N = 2010; const int mod = 1000000007; int x[N], y[N], xy[N][N], n, m, H, ans; int main() { int i, j, k, h; scanf("%d%d%d", &n, &m, &H); for(i = 1; i <= H; i ++){ scanf("%d%d%d", &j, &k, &h); x[j] += h, y[k] += h, xy[j][k] += h; } for(i = 1; i <= n; i ++) for(j = 1; j <= m; j ++) ans = (ans + (long long)(x[i] + y[j] - xy[i][j]) * (i + j) % mod) % mod; printf("%d", (ans + mod) % mod); return 0; }
G 括号序列
括号序列合法的一个充要条件是:
设'('为1,')'为-1,则:①序列所有位置前缀和非负;②'('与')'数量相等
为了保证条件1,我们可以用栈模拟括号序列的匹配过程
碰到一个'('就加入栈中,碰到一个')'就消去栈顶的一个'('
如果栈中没有没有'('则这个')'必须要删去
在这个过程结束之后,如果'('比')'多,则从后向前删去多余的'(',直到序列合法即可
可以证明这样一定能得到满足要求的括号序列:两个步骤中删除的括号都是必须删去的
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; const int N = 1000050; int T, n, l, r, sum, ans; char s[N]; int main() { int i, j, k; scanf("%d", &T); while(T --){ scanf("%d", &n); scanf("%s", s + 1); l = r = sum = ans = 0; for(i = 1; i <= n; i ++){ if(s[i] == '(') l ++, sum ++; else if(sum > 0) r ++, sum --; else ans ++; } printf("%d\n", ans + l - r); } return 0; }
H 云
直接考虑问题较难,因为两种云都在运动。
但是我们可以考虑相对运动,将这个过程等效为左下角的云朝右上方移动。
在这个背景下我们容易发现:将所有的云投影成y=-x这条直线上的一条线段,则两朵云会相遇当且仅当他们的投影有交点。
这是一个简单的扫描线问题,将线段拆成端点后排序统计即可。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long LL; const int N = 400050; struct node{ int loc, lr, typ; inline bool operator < (const node &b)const{ if(loc == b.loc && lr == b.lr) return typ < b.typ; if(loc == b.loc) return lr > b.lr; return loc < b.loc; } }A[N]; int n, m, a, b, c, d, k; LL cnt[2], ans; int main() { for(scanf("%d%d", &n, &m); n --;){ scanf("%d%d%d%d", &a, &b, &c, &d); A[++ k] = (node){d - c, 1, 0}; A[++ k] = (node){b - a, -1, 0}; } while(m --){ scanf("%d%d%d%d", &a, &b, &c, &d); A[++ k] = (node){d - c, 1, 1}; A[++ k] = (node){b - a, -1, 1}; } sort(A + 1, A + k + 1); for(int i = 1; i <= k; i ++){ cnt[A[i].typ] += A[i].lr; if(A[i].lr == 1) ans += cnt[A[i].typ ^ 1]; } printf("%lld\n", ans); return 0; }
I 导航系统
显然数据给出的原图是一棵树。
容易发现,如果将输入的N(N-1)个距离看做N(N-1)条无向边的话,那么如果数据合法,原树就是这张新图的最小生成树。
证明:由于边权是非负的,可以考虑Kruskal算法的过程,每一次引入的边都是尽可能短的,所以一定是树中的边,通过简单的归纳即证。
所以求出最小生成树之后再进行验证就好了。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; const int N = 510; struct edge{ int u, v, c; bool operator < (const edge &b)const{ return c < b.c; } }w[N * N], ans[N]; int a[N][N], n, m = 0, T[N], to[N][N], siz[N], len[N][N], dis[N][N]; int path(int x){ if(T[x] == 0) return x; return T[x] = path(T[x]); } void dfs(int x, int fa, int st, int d){ dis[st][x] = d; for(int i = 1; i <= siz[x]; i ++) if(to[x][i] != fa) dfs(to[x][i], x, st, d + len[x][i]); } int main() { int i, j, k; scanf("%d", &n); for(i = 1; i <= n; i ++) for(j = 1; j <= n; j ++) scanf("%d", &a[i][j]); for(i = 1; i <= n; i ++){ for(j = 1; j <= n; j ++){ if(a[i][j] != a[j][i]){ puts("No"); return 0; } if(i < j){ m ++; w[m].u = i; w[m].v = j; w[m].c = a[i][j]; } } } sort(w + 1, w + m + 1); for(i = 1, j = 0; i <= m && j < n - 1; i ++){ int x = path(w[i].u), y = path(w[i].v); if(x != y){ T[x] = y; ans[++ j] = w[i]; siz[ans[j].u] ++, siz[ans[j].v] ++; to[ans[j].u][siz[ans[j].u]] = ans[j].v; to[ans[j].v][siz[ans[j].v]] = ans[j].u; len[ans[j].u][siz[ans[j].u]] = len[ans[j].v][siz[ans[j].v]] = ans[j].c; } } for(i = 1; i <= n; i ++) dfs(i, 0, i, 0); for(i = 1; i <= n; i ++){ for(j = 1; j <= n; j ++){ if(a[i][j] != dis[i][j]){ puts("No"); return 0; } } } puts("Yes"); for(i = 1; i < n; i ++) printf("%d\n", ans[i].c); return 0; }
J 签到题
对没错它就是签到题,只要会解一个简单的方程就好了。
可以证明“No”是不存在的,所以不论怎么写都能对。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> using namespace std; double a[3], b[3]; int main() { cin >> a[0] >> a[1] >> a[2]; sort(a, a + 3); if(a[0] + a[1] <= a[2]){ puts("wtnl"); return 0; } puts("Yes"); b[1] = (a[2] - a[1] + a[0]) / 2; b[0] = a[0] - b[1], b[2] = a[2] - b[1]; printf("%.2lf %.2lf %.2lf", b[0], b[1], b[2]); return 0; }到此整个集训营就结束啦!祝大家的AK姿势都能有进步!
如果这场比赛给你带来了收获,就是坠吼的!