20201101gryz模拟赛解题报告
写在前面
2020rp++
停课的第一场模拟赛
拿上一年的上一年的day1来考的,
结果得分期望220pts,实际135pts,rank3,太菜了
考着考着机房灯突然灭了,当时慌的一批
以为断电代码要没了,还好电脑没事
T1铺设道路
这次T1不想上次那么暴力,一开始感觉还挺有思考量
后来发现可以分治做,但我那个板子好像不会打(我对不起Lucky_block)
然后就整了个线段树来维护,并且只需要建树和上传即可,所以还是蛮简单的
合并过程的原理和正解差不多(赛后我才知道差分O(n)就能过)
需要注意的是在上传的时候要先取和再减去左子树右端点和右子树左端点中的最小值
1 #include<iostream> 2 #include<cstdio> 3 #include<cmath> 4 #define lson i << 1 5 #define rson i << 1 | 1 6 using namespace std; 7 const int MAXN = 1e5+5; 8 struct tree{ 9 int sum, l, r; 10 }tree[MAXN << 2]; 11 int n; 12 int a[MAXN]; 13 14 int read(){ 15 int s = 0, w = 1; 16 char ch = getchar(); 17 while(ch < '0' || ch > '9') {if(ch == '-') w = -1; ch = getchar(); } 18 while(ch >= '0' && ch <= '9') s = (s << 1) + (s << 3) + ch - '0', ch = getchar(); 19 return s * w; 20 } 21 22 void push_up(int i){ 23 tree[i].sum = (tree[lson].sum + tree[rson].sum) - ( min(a[tree[lson].r], a[tree[rson].l]) ); 24 return ; 25 } 26 27 void build(int i, int l, int r){ 28 tree[i].l = l, tree[i].r = r; 29 if(l == r) { 30 tree[i].sum = a[l]; 31 return ; 32 } 33 int mid = (l + r) >> 1; 34 build(lson, l, mid), build(rson, mid + 1, r); 35 push_up(i); 36 return ; 37 } 38 39 int main() 40 { 41 freopen("road.in","r",stdin); 42 freopen("road.out","w",stdout); 43 44 n = read(); 45 for(int i = 1; i <= n; ++i) a[i] = read(); 46 47 build(1, 1, n); 48 49 printf("%d", tree[1].sum); 50 51 return 0; 52 }
T2货币系统
一开始看错了,以为就是个nlogn的暴力枚举,打完才发现
然后开始想正解,正解没想出来
看到部分分的数据比较小
就开始打暴力,三个小数据点都过了,后来下发的大数据点没过
但当时没找到错误就抱着随缘的心态放了过去,期望80pts,实际25pts
正解:(zzg这就是个完全背包板子)新求的集合一定是原集合子集,并且如果原集合中的某个值珂以用两个更小的值凑出来的话(每个值可以用无限次)那么它珂以被删掉
因为一个面值小的钱不可能被大的合成,所以从小到大排序,并从小到大枚举
转移方程:f[j] |= f[j - a[i]];
f[j]用来记录前面的钱能否合成j这个面值的钱,如果减掉a[i]的那个钱能被合成,那么加上此时枚举到的a[i]的这个钱也可以被合成
1 /* 2 Work by: Suzt_ilymics 3 Knowledge: ?? 4 Time: O(??) 5 */ 6 #include<iostream> 7 #include<cstdio> 8 #include<cstring> 9 #include<algorithm> 10 using namespace std; 11 int T, n; 12 int a[110]; 13 bool f[25100]; 14 15 int read(){ 16 int w = 1, s = 0; 17 char ch = getchar(); 18 while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar(); } 19 while(ch >= '0' && ch <= '9') s = (s << 1) + (s << 3) + ch - '0', ch = getchar(); 20 return s * w; 21 } 22 23 int main() 24 { 25 T = read(); 26 while(T--){ 27 memset(f, 0, sizeof(f)); 28 n = read(); 29 int cnt = n; 30 for(int i = 1; i <= n; ++i) a[i] = read(); 31 sort(a + 1, a + n + 1); 32 f[0] = 1; 33 for(int i = 1; i <= n; ++i){ 34 if(f[a[i]]) {cnt--; continue; } 35 for(int j = a[i]; j <= a[n]; ++j){ 36 f[j] |= f[j-a[i]]; 37 } 38 } 39 printf("%d\n", cnt); 40 } 41 return 0; 42 }
T3赛道修建
想了一会发现自己根本不会,
然后老老实实的骗部分分,
1、m == 1
发现此时就是求多源最长路,然后莽上floyd(我当时并不知道floyd不能求最长路,但赛后想到可以转化成负边来求啊
发现不对,只好先放过
2、ai == 1
画个样例不难发现这是个“菊花图”,当时做法是排个序然后最大值和最小值配对,在从前到后扫一遍求出最小值来
(没错,这个贪心也是错的,当时我sb没看出来,还以为能骗个20分)
3、bi == ai + 1
可以发现这是条链,这很显然的二分板子啊,那这个20分不就到手了
(Q:你不会二分板子也写错了吧? A:没错,我二分又写挂了)
正解:(从题解上爬的加上老吕讲的
看到题意描述第一反应就是先二分那个修建的 m 条赛道中长度最小的赛道的长度 k ,然后 O(n) 或 O(nlogn) 判断。
那么怎么判断呢?
对于每个结点,把所有传上来的值 val 放进一个 multiset ,其实这些值对答案有贡献就两种情况:
- val ≥ k
- val_a + val_b ≥ k
那么第一种情况可以不用放进 multiset,直接答案 +1 就好了。第二种情况就可以对于每一个最小的元素,在 multiset 中找到第一个 ≥ k 的数,将两个数同时删去,最后把剩下最大的值传到那个结点的父亲
有没有可能对于有些情况直接传最大的数会使答案更大?
当然不会。这个数即使很大也只能对答案贡献加 1,在其没传上去的时候可以跟原来结点的值配对,也只能对答案贡献加 1。
miltiset用法(一篇不错的博客)(我没时间整理只能搬网址了)
1 /* 2 Work by: Suzt_ilymics 3 Knowledge: ?? 4 Time: O(??) 5 */ 6 #include<iostream> 7 #include<cstdio> 8 #include<set> 9 using namespace std; 10 const int MAXN = 5e4+5; 11 struct edge{ 12 int to, w, nxt; 13 }e[MAXN << 1]; 14 int head[MAXN], num_edge; 15 int n, m, ans, cnt, up; 16 int sum1, sum2; 17 18 multiset<int> s[MAXN]; 19 multiset<int>::iterator it; 20 /*c++语言中,multiset是<set>库中一个非常有用的类型, 21 它可以看成一个序列, 22 插入一个数,删除一个数都能够在O(logn)的时间内完成, 23 而且他能时刻保证序列中的数是有序的, 24 而且序列中可以存在重复的数。*/ 25 int read(){ 26 int w = 1, s = 0; 27 char ch = getchar(); 28 while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar(); } 29 while(ch >= '0' && ch <= '9') s = (s << 1) + (s << 3) + ch - '0', ch = getchar(); 30 return s * w; 31 } 32 33 void add(int from, int to, int w){ 34 e[++num_edge] = {to, w, head[from]}, head[from] = num_edge;//更为简洁的存边方式 35 // e[++num_edge].to = to; 36 // e[num_edge].w = w; 37 // e[num_edge].nxt = head[from]; 38 // head[from] = num_edge; 39 } 40 41 int dfs(int x, int fa, int mid){ 42 s[x].clear();//先把数组清空 43 int val; 44 for(int i = head[x]; i; i = e[i].nxt){ 45 int v = e[i].to; 46 if(v == fa) continue; 47 val = dfs(v, x, mid) + e[i].w;//以x为根能用的最长链是儿子的最长链加上与儿子相连的那条链 48 if(val >= mid) cnt++;//如果某条链达到mid直接++ 49 else { 50 s[x].insert(val);//否则暂时插入s[x]序列中 51 } 52 } 53 int Max = 0; 54 while(!s[x].empty()){//如果序列不为空(即还有链没用到 55 if(s[x].size() == 1) { 56 return max(Max, *s[x].begin());//如果只有一个元素直接返回Max 57 } 58 it = s[x].lower_bound(mid - *s[x].begin());//找到一个能与s[x]的第一个元素相加超过mid的元素(这个元素是刚刚使条件成立,保证不浪费 59 if(it == s[x].begin() && s[x].count(*it) == 1) it++;//如果这两个数相同并且it的数量等于1,就使it的指向下一个元素 60 if(it == s[x].end()){//如果没有这个元素 61 Max = max(Max, *s[x].begin());//最大值返回第一个值 62 s[x].erase(s[x].find(*s[x].begin()));//清除第一个值 (即这个数暂时不可用 63 } 64 else { 65 cnt++;//如果能用,就意味着配对成一条链 66 s[x].erase(s[x].find(*it));//清除两个元素 67 s[x].erase(s[x].find(*s[x].begin())); 68 } 69 } 70 return Max;//返回能用的最大值 71 } 72 73 int check(int mid){ 74 cnt = 0; 75 dfs(1, 0, mid); 76 return cnt >= m;//如果以mid为界能分的链大于等于m,返回1,向上二分,反之返回0,向下二分 77 } 78 79 int dfs1(int x, int fa){// 80 int sum1 = 0, sum2 = 0;//sum1存以x为根节点的最长链,sum2存以x为根节点的次长链 81 for(int i = head[x]; i; i = e[i].nxt){ 82 int v = e[i].to; 83 if(v == fa) continue; 84 sum2 = max(sum2, dfs1(v, x) + e[i].w); //最长链的长度 = 每个子节点中的最长链加上它到自己儿子的这条边 85 if(sum1 < sum2) swap(sum1, sum2);//判断以x为根节点的次长链能否成为最长链 86 } 87 up = max(up, sum1 + sum2); 88 return sum1; 89 } 90 91 int main() 92 { 93 n = read(), m = read(); 94 for(int i = 1, u, v, w; i < n; ++i){ 95 u = read(), v = read(), w = read(); 96 add(u, v, w), add(v, u, w);//存边 97 } 98 dfs1(1, 0); 99 int l = 1, r = up;//up表示树上最长链(答案上界 100 while(l <= r){//二分答案 101 int mid = (l + r) >> 1; 102 if(check(mid)) { 103 ans = mid, l = mid + 1; 104 } 105 else r = mid - 1; 106 } 107 printf("%d\n", ans); 108 return 0; 109 }
总结
1、T3还是不怎么懂,以后有时间要再看看
2、我代码能力还是太弱了,一个暴力一个贪心一个二分都写挂了
3、这么久我还是一个dp小白