比赛结束了几天...这篇博客其实比完就想写了...但是想等补完可做题顺便po上题解...
5.10晚的动车到了济南,没带外套有点凉。酒店还不错。
5.11早上去报道,济南大学好大啊...感觉走了一个世纪才到了他们的教学楼...跟我们学校比起来...
中午吃的他们的八食堂,对于我来说并没有吃饱,食堂看着很好但是给队员的窗口只有一个。其他的都没办法吃。
下午热身赛AB水题。C题刚开始觉得是二分,中途发现二分是错的然后改了几发过了之后就走了。出来之后才看的D,才知道为啥要放弃D了233
晚上吃饭吃得有点晚赶不回去打Atcoder。回到酒店后躺了会床。洗完澡11点多了就把Atcoder的ABCD切了,12点多就上床睡了(主要是E题不会写。待会去补。
第二天比赛,拿到题册后想着要抢一发一血,先开的A题,一紧张写WA了。改了一下才过的。之后看了下D发现跟图无关,可以直接写就一发过了。
然后我就贡献了3发H的罚时啦。然后就没啦。全靠队友带躺走进金牌区。封榜后看出E的ans怎么统计但是就是不知道用啥数据结构维护。算是比较遗憾的啦。
其实我很不想被人带躺。进了金牌区我并不觉得很开心甚至觉得很丢脸。因为我对队伍的贡献就只有傻逼的罚时和一两道签到题。还是得变强啊。
定位:签到
题意:重新定义了一年有12个月,1个月有30天,一周只有5天。给出一个年月日加上它的星期数,求另外一个年月日是星期几。
思路:求出两个日期之间的天数差,然后把星期几给加上这个差再mod 5就好了。注意负数的情况。比赛的时候想复杂了。还分类讨论了。。
#include <bits/stdc++.h> #define ll long long using namespace std; const char s[5][20] = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"}; char str[20]; int main() { int T; scanf("%d", &T); while (T--) { ll a, b, c, d, e, f; scanf("%lld%lld%lld", &a, &b, &c); scanf("%s", str); scanf("%lld%lld%lld", &d, &e, &f); ll sum1 = a * 30 * 12 + b * 30 + c; ll sum2 = d * 30 * 12 + e * 30 + f; ll temp = sum2 - sum1; ll id = 0; for (int i = 0; i < 5; i++) { if (strcmp(str, s[i]) == 0) { id = i; break; } } id += temp; id %= 5; while (id < 0) id += 5; puts(s[id]); } return 0; }
定位:DP,组合数学,难
比赛的时候完全不懂。
题意:两个长度为n的01串分别为初态和末态,要求操作K轮,每轮操作m次,每次是把0变成1或者1变成0,要求从初态到末态有多少种方案。
思路:问题可以进一步简化,只要看初末有多少个字符不同,设为x。
$dp\left[ p\right] \left[ i\right]$表示在第$p$轮里有$i$个字符不同要变成相同的方案数
转移方程就是下面这样
$dp\left[ p\right] \left[ i\right] =\sum ^{\min \left( i,m\right) }_{j}C_{i}^{j} C_{n-i}^{m-j}dp\left[ p-1\right] \left[ i-j+m-j\right]$
初始:$dp\left[ 0\right] \left[ 0\right] = 0$ 第0轮0个需要变的方案肯定为1嘛。
对转移方程的解释是,在$i$个不同的里面挑了$j$来变成相同的,那么现在就还有$i-j$个是不同的,而在$n-i$个相同的里面挑了$m-j$个来变成不同的 所以这一轮之前需要变$i-j+m-j$个。
组合数就是由于顺序的不同导致方案数变多。其实还有一个$m-j$小于等于$n-i$的限制,但是由于组合数我只求到了相等的时候 所以比它大的时候组合数为0,答案不会受到影响。
#include <bits/stdc++.h> #define ll long long using namespace std; const int N = 200; const ll MOD = 998244353; ll C[N][N], dp[N][N]; void init() { C[0][0] = 1; C[1][0] = C[1][1] = 1; for (int i = 2; i < N; i++) { C[i][0] = 1; for (int j = 1; j <= i; j++) C[i][j] = C[i-1][j] + C[i-1][j-1], C[i][j] %= MOD; } } char num[2][N]; int main() { init(); int T; scanf("%d\n", &T); while (T--) { memset(dp, 0, sizeof(dp)); int n, m, k; scanf("%d%d%d", &n, &k, &m); int x = 0; scanf("%s", num[0]); scanf("%s", num[1]); for (int i = 0; i < n; i++) x += num[0][i] != num[1][i]; dp[0][0] = 1; for (int p = 1; p <= k; p++) { for (int i = 0; i <= n; i++) { for (int j = 0; j <= i && j <= m; j++) { dp[p][i] += dp[p - 1][i - j + m - j] * C[i][j] % MOD * C[n - i][m - j] % MOD; dp[p][i] %= MOD; } } } printf("%lld\n", dp[k][x]); } return 0; }
定位:签到,思维
题意:一个机器人从坐标原点出发,前n步的走法给定(上下左右之一)后面的走法循环,总共走nk步,求在哪个位置离原点最远(曼哈顿距离)
思路:场上队友写的。答案的出现必定是在第一轮或者最后一轮里,因为假如第二轮中出现了一个点能比第一轮走得远,那么第三轮也可以有一个点走得比第二轮远(以此类推)
因此答案就在第一轮或者第k轮出现
然后首先把第一轮的$n$步所到达的位置记录下来,最后一轮第$i$步的坐标就是$\left(\left( k-1\right) x_{n}+x_{i},\left( k-1\right) y_{n}+y_{i}\right)$再统计一边答案即可
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 10; char s[N]; long long dp[N]; struct P { long long x, y; } p[N]; int main() { int T; scanf("%d", &T); while (T--) { int n, k; scanf("%d%d", &n, &k); scanf("%s", s); long long x = 0, y = 0; long long mm = 0; for (int i = 0; i < n; i++) { if (s[i] == 'U') y++; if (s[i] == 'L') x--; if (s[i] == 'R') x++; if (s[i] == 'D') y--; p[i].x = x, p[i].y = y; mm = max(mm, abs(x) + abs(y)); } for (int i = 0; i < n; i++) { dp[i] = abs((k-1) * p[n-1].x + p[i].x) + abs((k - 1) * p[n-1].y + p[i].y); mm = max(dp[i], mm); } printf("%lld\n", mm); } return 0; }
定位:对图和树的理解,思维,简单题
题意:有k个人要么是1队的要么是2队的。给一个图n个点m-1条边,每个人删一条边(按给定的字符串的顺序),到谁删了一条边是图不连通了即输
思路:因为每个人都肯定选择最优的方法,只有到了剩下一棵树的时候才不能再删了,因此就是有(m-n+1)删除的机会,取个模判断一下就完了
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 10; char s[N]; int main() { int T; scanf("%d", &T); while (T--) { int k; scanf("%d", &k); scanf("%s", s); int n, m; scanf("%d%d", &n, &m); for (int i = 0, u, v; i < m; i++) scanf("%d%d", &u, &v); m -= n - 1; m %= k; printf("%c", s[m] == '1' ? '2' : '1'); puts(""); } return 0; }
定位:数据结构,思维,难
题意:baobao喜欢看书,有一个书架上面有他想看的书,一个序列表示他想看书的顺序,桌子有一定的容量,所以当看得书多了就必须放回去,给了一个算法,按照那个算法问桌子容量为1到n时要操作的次数
思路:初始的答案都是n次,给的算法就像一个窗口,每两个相同相邻数字之间不同数字的个数id会使容量为id的操作次数减一
比如题目中的序列
4 3 4 2 3 1 4 第一对相同相邻数字 4 3 4之间有2种不同的数字那么ans[2]--, 即ans[2] = 6
第二对 3 4 2 3 ans[3] = 6 第三对 4 2 3 1 4 ans[4] = 6; 然后把在id之前减的数叠加一下
就有了ans[1] = 7, ans[2] = 6, ans[3] = 5, ans[4] = 4, 接下去都是4
所以相当于所有相同相邻的数字的位置相当于一次查询,把所有查询后的答案统计出来就可以了。
赛场上想到这就不会了。出来后另一支队跟我们说是一道原题(平时不刷题,比赛两行泪 BZOJ1878: [SDOI2009]HH的项链
看了下黄学长的思路
用树状数组离线处理,把所有第一次出现的数字的位置上前缀和+1
把每个数字对应的下一个出现的位置的下标处理出来,没有下一个了即为0 即为Next[i]
然后把所有的查询按左右端点排序,用一个l扫描线,查询之前把该查询左端点的左端点的Next[i]的位置加一 到了左端点就查询答案
上面思路的意思就是
刚开始把所有颜色的第一个位置标记一遍,如果查询的左端点在扫描线之后,那么扫描线这个位置必定是被标记过了,即+1了
查询的时候就有可能会少一种颜色,所以必须把Next[i]位置也+1了 直到到了查询位置才不加
例如样例
a :4 3 4 2 3 1 4
sum:1 2 2 3 3 4 4
第一次查询(下标)是1和3 l为1 可以直接得到答案2
第二次查询为2和5 l还没到2 那么Next[1]=3的位置要加1
a :4 3 4 2 3 1 4
sum:1 2 3 4 4 5 5
查询就会查出3 以此类推答案就出来了
相当于每一次查询的左端点之前位置的数字都失效了 所以必须把左端点及其之后的每一个数字第一次出现的位置给点亮才不会影响答案。
#include <bits/stdc++.h> using namespace std; inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); } return x * f; } const int N = 1e5 + 10; int a[N], p[N], sum[N], Next[N], ans[N]; int n; struct P { int l, r; bool operator < (const P &rhs) const { return l == rhs.l ? r < rhs.r : l < rhs.l; } } q[N*10]; inline int lowbit(int x) { return x & (-x); } void add(int x) { while (x <= n) { sum[x]++; x += lowbit(x); } } int query(int x) { int temp = 0; while (x > 0) { temp += sum[x]; x -= lowbit(x); } return temp; } int main() { int T = read(); while (T--) { n = read(); for (int i = 1; i <= n; i++) p[i] = sum[i] = Next[i] = ans[i] = 0; int k = 0; int cnt = 0; for (int i = 1; i <= n; i++) a[i] = read(), k = max(k, a[i]); for (int i = n; i >= 1; i--) { Next[i] = p[a[i]], p[a[i]] = i; if (Next[i]) { q[++cnt].l = i; q[cnt].r = Next[i]; } } for (int i = 1; i <= k; i++) if (p[i]) add(p[i]); sort(q + 1, q + 1 + cnt); int l = 1; for (int i = 1; i <= cnt; i++) { while (l < q[i].l) { if (Next[l]) add(Next[l]); l++; } int x = query(q[i].r) - query(q[i].l - 1); ans[x]++; } for (int i = 1; i <= n; i++) { if (i - 1) putchar(' '); ans[i] += ans[i-1]; printf("%d", n - ans[i]); } puts(""); } return 0; }
定位:签到
题意:n堆石子,可以移掉一个石子或者把一个移到另一个上,问最少要几次让它们个数相等
思路:求个平均值,然后大于平均值的就直接移就好了
#include <cstdio> #define ll long long using namespace std; inline ll read() { ll x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); } return x * f; } const int N = 1e5 + 10; ll a[N]; int main() { ll T = read(); while (T--) { ll n = read(); ll sum = 0; for (int i = 0; i < n; i++) a[i] = read(), sum += a[i]; ll temp = sum / n; ll ans = 0; for (int i = 0; i < n; i++) { if (a[i] > temp) ans += a[i] - temp; } printf("%lld\n", ans); } return 0; }
定位:贪心,中等
题意:一堆线段,问最多多少条线段上能加点并且这些点的位置不会一样(题目是二维,要求横坐标x不一样,如果变成一维就直接是坐标不一样)
思路:比赛的时候写了三发假贪心都没能想到正解,这是队友糊了发思路是对,但是复杂度玄学的代码上去了,由于数据水给过了。回来实测n=5e3的 l = 1, r = 1e6的数据要跑6s 2333
但是代码的贪心思想是对的。先把所有区间按左端点和右端点从小到大排。然后取出第一个,答案+1,然后把之后所有$l$等于该l的左端点都加一,小于等于右端点的给放进去,重复这个过程
就是因为我取完第一条线段的左端点,就相当于当前所有可用区间就是$\left[ l+1,+\infty \right)$,所以重新排一次序即可,用优先队列比较方便。但是。n=5e3,$l=1$,$r=1e6$的数据就已经炸了
因为一个点可能进出队列$n$次,复杂度就会达到$O\left( n^{2}\log n\right)$就炸了。
正解是先按左端点和右端点从小到大排,用一根扫描线$x$从$p\left[ 1\right] .l$开始往右扫,把所有左端点与$x$相同的线段的右端点放进优先队列里,再取出一个,大于等于$x$的就让答案+1
如果当前队列为空,$x$直接跳到下一个$l$,优先队列维护的就是$r-x$最小
#include <bits/stdc++.h> using namespace std; const int N = 1e5 + 10; struct P { int l, r; bool operator < (const P &rhs) const { if (l == rhs.l) return r < rhs.r; return l < rhs.l; } } p[N]; int main() { int T; scanf("%d", &T); while (T--) { int n; scanf("%d", &n); memset(p, 0, sizeof(p)); int mm = 0; for (int i = 1; i <= n; i++) { scanf("%d%d", &p[i].l, &p[i].r); mm = max(mm, p[i].r); } priority_queue<int, vector<int>, greater<int> > que; while (!que.empty()) que.pop(); sort(p + 1, p + n + 1); int ans = 0; int i = 1; int x = p[1].l; while (x <= mm) { for (;p[i].l == x; i++) que.push(p[i].r); while (!que.empty()) { int q = que.top(); que.pop(); if (q >= x) { ans++; break; } } if (que.empty()) { x = p[i].l; if (i > n) break; } else x++; } printf("%d\n", ans); } return 0; }
定位:数论,难
题意:给定$a$和$p$,求出所有的$x$使$a^{x}\equiv x^{a}\left( mod2^{p}\right)$,$x$在$\left[ 1,2^{p}\right]$
思路:反正我一般是写不出来的。打个表发现奇数的时候都为1。
考虑偶数的情况。
设$a=k\cdot 2^{q}$ $a^{x}=2^{qx}\cdot k^{x}$
下面就把$k^{x}$给略去了...
如果$a^{x}\% 2^{p}=0$
$2^{qx}\% 2^{p}=0$
$qx\geq p$
$x\geq \lceil \frac{p}{q}\rceil$
此时$x^{a}\% 2^{p}=0$
设$x=l\cdot 2^{m}$ 下忽略$l$
$x^{a}=2^{ma}$
由$x^{a}\% 2^{p}=0$
可得$ma\geq p$
$m\geq \lceil \frac{p}{a}\rceil$
则$x$为$\left[\lceil \frac{p}{q}\rceil,2^{p}\right]$间$2^{\lceil \frac{p}{a}\rceil}$的倍数
所以ans += $\frac{2^{p}}{2^{\lceil \frac{p}{a}\rceil}}- \frac{\lceil \frac{p}{q}\rceil}{2^{\lceil \frac{p}{a}\rceil}}+ \left(\lceil \frac{p}{q}\rceil \% 2^{\lceil \frac{p}{a}\rceil} == 0 \right)$
当$a^{x}\% 2^{p}\neq0$
$x\leq \lceil \frac{p}{q}\rceil$
由于$p$很小,那么$x$也会很小,for循环暴力求即可。
#include <bits/stdc++.h> #define ll long long using namespace std; ll qp(ll a, ll b, ll mod) { ll ans = 1; while (b) { if (b & 1) ans = ans * a % mod; a = a * a % mod; b >>= 1; } return ans; } int main() { int T; scanf("%d", &T); while (T--) { ll a, p; scanf("%lld%lld", &a, &p); ll temp = a; ll q = 0; while (temp % 2 == 0) q++, temp /= 2; if (q == 0) { puts("1"); continue; } ll ans = 0; ll mod = 1 << p; ll cnt1 = p / q + (p % q != 0); ll cnt2 = p / a + (p % a != 0); cnt2 = 1 << cnt2; for (int x = 1; x * q < p; x++) { if (qp(a, x, mod) == qp(x, a, mod)) ans++; } ans += mod / cnt2 - cnt1 / cnt2 + (cnt1 % cnt2 == 0); printf("%lld\n", ans); } return 0; }
定位:图,思维,中等
题意:给定一些元素的大小关系,要求把序列中可能为$\dfrac {n+1}{2}$大的元素输出为1,不存在准确的关系则输出全0字符串
思路:我一直觉得是个拓扑排序,好像别的队都是两遍拓扑排序过的。但我就是不会用拓扑排序求。Floyd可以很好地求解。判环+统计每个点所能到达的点的个数
当自己能到达的点和能到达自己的点均小于等于$\frac {n}{2}$时即为1
#include <bits/stdc++.h> using namespace std; const int N = 110; bool G[N][N]; int n, m, to[N][2]; bool floyd() { for (int k = 1; k <= n; k++) { for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { G[i][j] |= G[i][k] && G[k][j]; } } } for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (G[i][j] && G[j][i]) return 0; if (G[i][j]) to[i][0]++, to[j][1]++; } } return 1; } int main() { int T; scanf("%d", &T); while (T--) { memset(G, 0, sizeof(G)); memset(to, 0, sizeof(to)); scanf("%d%d", &n, &m); while (m--) { int u, v; scanf("%d%d", &u, &v); G[u][v] = 1; } if (!floyd()) for (int i = 0; i < n; i++) putchar('0'); else { for (int i = 1; i <= n; i++) { if (to[i][0] <= n / 2 && to[i][1] <= n / 2) putchar('1'); else putchar('0'); } } puts(""); } return 0; }
定位:签到
题意:给一个数n和操作次数k,每次操作都变成$\lceil \frac{n}{2}\rceil$
思路:int以内的一个数最多可操作31次。之后会是1,除非n=0时结果会是0
所以每次都模拟一次除二向上取整,然后<=1就可以输出了
#include <bits/stdc++.h> using namespace std; inline int read() { int x = 0, f = 1; char ch = getchar(); while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); } while (ch >= '0' && ch <= '9') { x = x * 10 + ch - 48; ch = getchar(); } return x * f; } int main() { int T = read(); while (T--) { int n = read(), k = read(); for (int i = 0; i < k; i++) { if (n % 2) n = n / 2 + 1; else n /= 2; if (n == 0 || n == 1) break; } printf("%d\n", n); } return 0; }