而且赛时排名恰好 , 所以有了这张封面图。
]]>而且赛时排名恰好 , 所以有了这张封面图。
修正排名 , 个人最好成绩。
虽然最后的成绩非常不错,但是中间的失误还是有点多的,也险些只做出两道,那样就会很严重的掉分了。
比赛开始 A 题还是做的比较顺利的,然后 B 题就直接翻车了,明明是很简单的一题 WA 了一次又 T 了一次, 可是这题才 1200……, 然后 C 题就直接弄的人很绝望,看着一千多人过了自己却没有一点思路。
幸好 D 题一下子就大概知道了,结果还没有想清楚,WA 了一次,然后换上 vector
, 可这玩意儿居然插入速度没有想象中的那么快,换成 list
才过。
将
D
和K
组成的字符串分成D
个数和K
个数比值相同的子串,求最多的串个数。
这里的比值的是可以有 的,非常讨厌,所以显然先把前面一段只有单个的给处理好去掉。
然后考虑后面怎么做——然后我就没有思路了,一直到最后才突然开窍。
后面比值是正常的,所以可以约分当做正常分数来算。那么很明显的,对 D
和 K
的个数做前缀和,当 D
和 K
成一定比例时,它们前缀和的差一定是成相同比例的,而第一段是从头开始的,所以就变成了前缀和的比例相同,所以定义个 twt
把那些分数记下来用个 map
存就好了。
就这要我想一个小时???
#include <cstdio>
#include <map>
const int N = 500005;
int gcd(int a, int b) {
if(b == 0) return a;
return gcd(b, a % b);
}
struct twt {
int a, b;
void pro() {
int g = gcd(a, b);
a /= g, b /= g;
}
bool operator < (twt y) const {
return a < y.a || (a == y.a && b < y.b);
}
};
std::map<twt, int> map;
int n, ans[N], T;
char st[N];
int main() {
scanf("%d", &T);
while(T--) {
map.clear();
scanf("%d%s", &n, st+1);
int a = 0, b = 0, f = 1;
for( ; st[f+1] == st[1]; f++);
for(int i = 1; i <= f; i++) ans[i] = i;
if(st[1] == 'D') a = f; else b = f;
for(int i = f+1; i <= n; i++) {
twt now;
a += st[i] == 'D';
b += st[i] == 'K';
now.a = a, now.b = b;
now.pro();
ans[i] = map[now] + 1;
map[now] ++;
}
for(int i = 1; i <= n; i++) printf("%d ", ans[i]);
puts("");
}
return 0;
}
这个感觉很套路,居然也有 分?显然中位数最多只移动一个,所以把中间的一串记下来,然后看能不能插入,能插就插,能移就移,不行就不行就好了。所以需要数据结构支持快速插入,用 std::list
即可。
#include <cstdio>
#include <list>
std::list<int> que;
int T, n, x;
int main() {
scanf("%d", &T);
while(T--) {
scanf("%d", &n);
que.clear();
scanf("%d", &x);
que.push_back(x);
std::list<int>::iterator p = que.begin();
bool flag = 0;
for(int i = 2; i <= n; i++) {
scanf("%d", &x);
if(flag) continue;
if(x < *p) {
if(p == que.begin() || x > *((--p)++)) p = que.insert(p, x);
else if(x != *(--p)) flag = 1;
}
else if(x > *p) {
if((++p)-- == que.end() || x < *((++p)--)) p = que.insert(((++p)--), x);
else if(x != *(++p)) flag = 1;
}
}
if(flag) puts("NO");
else puts("YES");
}
return 0;
}
妙妙题啊!
输入一个
0
和#
组成的矩阵,#
可以任意填数字,求满足相邻两个数相差不超过 且若一个数严格大于 则必须严格大于其周围的至少一个数的矩阵个数。
其实把 的位置都确定下来之后这个图就是确定的。
为什么?
可以先考虑一排只有一个 的情况,那么就只能是从 开始向周围发散。然后可以把这个继续思考,一个 出去之后如果不继续增长的话这一边没有比它大的了就只能指望其它的边,可其它的方向如果要降下去的话不能重新上升了,因为会产生比周围小的。而下降若不到 有不行,因为最后一个就四周没有一个比它小的了。
所以每个位置上就只能是到离他最近的 的距离,这样的化就只要枚举每个位置是不是 就好了。
代码甚至比第一题短。
#include <cstdio>
const int P = 1000000007;
int T, n, m;
char s[2005];
int main() {
scanf("%d", &T);
while(T--) {
scanf("%d%d", &n, &m);
int an = 0;
for(int i = 1; i <= n; i++) {
scanf("%s", s+1);
for(int j = 1; j <= m; j++) an += s[j] == '#';
}
int ans = 1;
for(int i = 1; i <= an; i++) ans = ans * 2 % P;
printf("%d\n", (ans - (an == n*m) + P) % P);
}
return 0;
}
O 和 A 在 个格子组成的环上面游戏,两人轮流放置字母
a
或b
, 不能让相同的字母相邻,若一个人无法放置字母,他就输了。求两人都按最优策略的方案数。
这个题目阴险啊!他跟你说是说要最优策略的方案数,然后你手玩一下发现不管怎么搞后手都一定赢。证明考虑最终状态是什么样子的,容易得到它必然是 ab
相间的,且可以有一些空格,不过空格不能连续,且空格两边也是黑白相间的。那么 a
和 b
就需要相同的个数,也就是后手必胜。
所以就是要计算所有合法局的方案数。这又是 nottttttthy 最喜欢的组合数学题了。
考虑枚举空了 个点, 那么就要用 个空将 个分开,因为空格不能连续,所以方案数是 , 因为这个格子是环状的,所以需要考虑第一个格子时空的情况,那么就是在剩下的 里面选 个就可以了。
因为 a
和 b
可以互换,而且顺序可以随意打乱,所以答案还要乘上 。
代码。
#include <cstdio>
#define int long long
const int N = 1000005, P = 1000000007;
int fac[N], inv[N], n, ans;
int Pow(int a, int b) {
int an = 1;
while(b) {
if(b & 1) an = an * a % P;
a = a * a % P;
b >>= 1;
}
return an;
}
int C(int n, int m) { return fac[n] * inv[m] % P * inv[n-m] % P; }
signed main() {
scanf("%lld", &n);
fac[0] = 1;
for(int i = 1; i <= n; i++) fac[i] = fac[i-1] * i % P;
inv[n] = Pow(fac[n], P-2);
for(int i = n; i >= 1; i--) inv[i-1] = inv[i] * i % P;
for(int i = n&1; i <= n-i; i += 2) ans = (ans + 2*fac[n-i]*(C(n-i, i)+C(n-i-1,i-1)) % P) % P;
printf("%lld", ans);
return 0;
}
E 犯了一个重大失误,没有验证过二分性就直接上二分了,导致最后调了很久才发现这个问题并改正,不然肯定会掉很多的分了,那就成悲剧了。
主要讲 F。
用 和 的格子覆盖 的矩形的方案数。
注意到数据范围 ,那么显然就是要状压掉一列的内容了。
表示前 行最后一行是 (横着叉出去是 否则是 )的方案数。转移的话就是枚举前面一行的状态,然后再枚举一下空的哪些用 $1\times 2 $ 哪些用 的就可以了。
但是一看 的范围是 这个范围也非常明显的是要让你矩阵乘法快速幂的优化的或者某些神奇的根号算法的。我们注意到 的转移里面枚举的哪些用 得出方案数的答案是固定的。所以可以把转移这样表示 ,后面的那个东西处理出来就是标准的矩阵乘法了。
说起来轻松但是当我自己写的时候还是遇到了很多的问题,大概是我对于矩阵乘法快速幂的理解不够深刻吧。
#include <cstdio>
#include <cstring>
#define int long long
const int P = 998244353;
int n, m;
struct twt {
int v[1 << 6][1 << 6], n, m;
int* operator [] (int x) { return v[x]; }
twt() { memset(v, 0, sizeof v); }
friend twt operator * (twt a, twt b) {
twt c;
c.n = a.n, c.m = b.m;
for(int i = 0; i < c.n; i++)
for(int j = 0; j < c.m; j++)
for(int k = 0; k < a.m; k++)
c[i][j] = (c[i][j] + a[i][k] * b[k][j] % P) % P;
return c;
}
} f, c;
twt Pow(twt a, int b) {
twt an = a; b--;
while(b) {
if(b & 1) an = an * a;
a = a * a;
b >>= 1;
}
return an;
}
signed main() {
scanf("%lld%lld", &n, &m);
f.n = 1, f.m = 1 << n;
f[0][0] = 1;
c.n = c.m = 1 << n;
for(int i = 0; i < (1 << n); i++)
for(int j = 0; j < (1 << n); j++)
for(int k = 0; k < (1 << n); k++)
if((i | j | k | (k << 1)) == i + j + k + (k << 1)
&& i + j + k + (k << 1) < 1 << n) c[i][j] ++;
f = f * Pow(c, m);
printf("%lld", f[0][0]);
return 0;
}
我遇到的问题大概都是在起始状态和停止状态上。
要记住,矩阵描述的是满足结合律的运算,所以直接把后面变换的那个 矩阵和我们原来的初始状态相乘就可以了。
然后发现按照《OI 比赛事故和事故征候评定标准》及其精神,这场比赛同样构成了轻微事故征候,只是最后 happy ending 了而已。
]]>然后发现按照《OI 比赛事故和事故征候评定标准》及其精神,这场比赛同样构成了轻微事故征候,只是最后 happy ending 了而已。
有点像 FM9246(错误连接 VOR,差点 CFIT,幸亏天气 CAVOK)。
在 ABC 完成之后都未按照标准操作程序进行备份,只在 C 题对拍之前想起来应该备份,而且在比赛开始后两小时十三分使用了回滚,如果没有想起需要备份,这次回滚将无法成功。
同时,C 题没有思考清楚,在写完之后调试了较久的时间,且依赖对拍来发现错误,没有考虑到前后奇偶的变化,在被野花骗之后没有仔细分析,最终在比赛结束前 分钟才完成检查单。
构成轻微事故征候。
花居然不写 SPJ 来卡精度??
不行,得自己写个 SPJ!
改了一份网上的 SPJ 代码,是比较九位浮点数的,放在这里备用吧。
#include <string>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <cmath>
using namespace std;
string answer,output;//标准输出和选手输出
FILE* fout, *fans, *flog;
string In, Out, Ans, Log;
void End(const string& info, double x, int state = 0){
fprintf(flog, "%.3lf\n%s\n", x, info.c_str());
exit(state);
}
void Open(){
if (Log.size()) flog = fopen(Log.c_str(), "w"); else flog = stdout;
if (flog == NULL) exit(1);
if ((fans = fopen(Ans.c_str(), "r")) == NULL) exit(1);
if ((fout = fopen(Out.c_str(), "r")) == NULL) exit(1);
}
bool check(string a, string b) {
for(int i = 0; i < min(9, (signed)b.size()); i++)
if(a[i] != b[i]) return true;
return false;
}
int main(int argc, char* argv[]){
In = "";
Ans = argc < 3 ? "" : argv[2];
Out = argc < 4 ? "" : argv[3];
Log = argc < 5 ? "" : argv[4];
//Ans和Out变量存储的是标准答案和选手答案的路径
Open();//初始化,这句很重要。
freopen(Ans.c_str(),"r",stdin);//重定向到标准输出
cin >> answer;//读入
freopen(Out.c_str(),"r",stdin);//重定向到选手输出
cin >> output;//读入
if (check(answer, output)){
End("与标准答案相差过大",0);
}else{
End("",1);
}
return 0;
}
]]>给定一个字符矩阵,可以修改 次,求左上到右下走所经过的字典序最小的字符串。
首先有个非常简单的想法,前面的 个肯定都是要改成 a
的。
因为输入串中可能也有一些 a
, 所以首先想到要找到一些位置使得从 到这里的路径能够全部被改成 , 可以很容易地用 dp 解决, 表示从 到 路径上最多的 a
的个数,若 那么就是可以的,若 那么就可以直接退出了。
因为我们要恰好用 个,所以考虑是否一定存在这样的位置使得 ( 不为 的时候)。
想想如果不存在这样的一个点会怎样,因为 的变化是连续的,而 的变化可能有跳跃,所以这种情况一定就是 只移动了 而 增加(或减少)了很多, 那么就继续移动这个点,如果 要维持他的优势不被 赶上,就得在赶上之前继续增加,而如果这样能一直增加下去,那么 就大于 了,与前提矛盾。
减小时情况同理。所以一定有至少一个这样的位置。
将这些位置找出来,然后的目标就是要找出后面的字典序最小是什么。
如果向下和向右不重复,那么直接走就行了,所以考虑若向下的和向右的是一样的情况。可以用 存下走到距离起点长度为 的点集合,然后向两个方向拓展,并把所有扩展到能拓展的最小字符的点全部放进 里,并且记下是从哪里来的,然后继续找。其实就是一个记下了每一步状态的 bfs。
因为每个点只需要入队一次,而只需要跑最多 次,所以这个搜索是 级别的、
实现上需要注意判断 的情况,还有一些细节就看代码吧。
自己写的时候感觉实现有一点丑陋,写完之后对比了一下现有题解,发现我写得还是挺短的。
代码。
#include <cstdio>
#include <vector>
#include <algorithm>
const int N = 2005;
int n, k, f[N][N];
bool vis[N][N];
int dx[3] = {0, 1, 0},
dy[3] = {0, 0, 1};
struct twt { int x, y, pre; };
std::vector<twt> d[N+N];
char ans[N+N], s[N][N];
int main() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++) scanf("%s", s[i]+1);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
f[i][j] = std::max(f[i-1][j], f[i][j-1]) + (s[i][j] == 'a');
if(k >= n+n-1 || f[n][n] + k > n+n-1) {
for(int i = 1; i <= n+n-1; i++) ans[i] = 'a';
printf("%s", ans+1);
return 0;
}
int max = 0;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(f[i][j] + k == i+j-1 && i+j-1 > max) max = i+j-1;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(f[i][j] + k == i+j-1 && i+j-1 == max)
d[i+j-1].push_back((twt){i, j, -1}), vis[i][j] = 1;
if(max == 0) d[1].push_back((twt){1, 1, -1});
for(int t = (max?max:1); t <= n+n-2; t++) {
char min = 'z';
for(int j = 0; j < (signed)d[t].size(); j++)
for(int k = 1; k <= 2; k++) {
int xx = d[t][j].x+dx[k], yy = d[t][j].y+dy[k];
if(xx <= n && yy <= n && !vis[xx][yy]) min = std::min(s[xx][yy], min);
}
for(int j = 0; j < (signed)d[t].size(); j++)
for(int k = 1; k <= 2; k++) {
int xx = d[t][j].x+dx[k], yy = d[t][j].y+dy[k];
if(xx <= n && yy <= n && !vis[xx][yy] && s[xx][yy] == min)
d[t+1].push_back((twt){xx, yy, j}), vis[xx][yy] = 1;
}
}
for(int i = 1; i <= max; i++) ans[i] = 'a';
for(int i = n+n-1, x = n, y = n, pre = d[n+n-1][0].pre; i > max; i--) {
ans[i] = s[x][y];
if(i != max+1)x = d[i-1][pre].x, y = d[i-1][pre].y, pre = d[i-1][pre].pre;
}
printf("%s", ans+1);
return 0;
}
]]>题目要求的等价于找出若干个 的约数(除了 )使得存在给每个配上一个系数让和为 。
想想什么情况没有解,若选的约数都不互质那没救了,因为 肯定和 是互质的,而选出来的这些数都是 的约数,它们加起来肯定是和 有除了 以外的因子的,而 和 没有,所以不可能。而若一个数只有单一质因子,那选出来的肯定互质不了。
那么一个质因子不行就想想两个可不可以做到。
就是要求出一个 , 且满足 。将 移到一边,变成 ,那么要求就变成要让这个式子的里的每个数都是正整数是否可行。因为 , 所以 ; 因为是整数,所以 。
对于后面一个条件,因为 是 的约数,所以 也得是 的约数,又因为 和 是互质的,所以 在模 意义下循环节长度是 ,所以当 取 时 恰好都被取到一次,也就是一定会有一个 。
再看前一个条件,等价于 , 因为 在 范围内时一定有满足后面条件的,所以一定存在满足条件的同时满足 , 因为 , 所以满足另一个条件的也一定满足 。
所以题目里什么 都是骗人的,只要两个就可以了。
具体地,先暴力找出 的所有质因子,然后选俩质因子枚举并判断是否满足条件就好了。
代码。
#include <cstdio>
int n, fac[105], m;
int main() {
scanf("%d", &n);
int n_ = n;
for(int i = 2; i*i <= n; i++)
if(n_ % i == 0) {
fac[++m] = i;
while(n_ % i == 0) n_ /= i;
}
if(n_ != 1) fac[++m] = n_;
if(m <= 1) return puts("NO"), 0;
puts("YES\n2");
int a, b, x = fac[1], y = fac[2];
for(a = 1; a <= y-1; a++)
if((a*x+1) % y == 0) { b = (n-1-a*x)/y; break; }
printf("%d %d\n%d %d", a, n/x, b, n/y);
return 0;
}
]]>FDR 的数据在这里可能不是那么的有用,因为只记录了每道题的完成时间和执行检查单的情况。在这次比赛之中,确实认真完成了各项检查单,在 C 题未调出而 D 未写的时候也执行了搁置程序。
但在做 A 和 D 的时候没有好好分析代码思路以至于在实现的时候遇上了各个本应避免的问题,包括 dp 含义不清等等。在 B 题中虽然是开小但这不应该属于检查单中的 range
, 因为是对组合数的使用范围产生了误解。
这次的事故征候从做 A 题的时候就开始了,因为没有搞清楚各个数组的状态含义使得浪费了大量的调试时间,B 题还算顺利,但因为 A 的时间浪费使得没有时间去调不大熟练的 C,同时在 C 题时没有仔细厘清代码思路,使得需要时间去调试。这些原因也导致了 D 的挂分,最后成了这个结局。
所以本次的事故征候原因主要在于标准程序的不完善。
掉分了。
居然被 C 给卡了一下,又被 D 卡。
给定一个 的矩阵,求其中所有 的子矩阵的中位数的最小值。
首先想到的是大力数据结构,树套树之类的,可是这东西不一定过得了,而且 ABC 的 D 怎么可能要数据结构。
然后就想来想去奇奇怪怪的做法,一段时间想着这似乎可以二分,但是很快就被我判定没有二分性了,可能是受到了前面一题枚举被我误认成二分的影响吧。
所以我还不理解二分,对吗。/dk
其实是有二分性的。我们可以转化一下这样一个问题,把它要求的变成要求最小的数 , 使存在至少一个 的矩阵中最多有 个比其小,这样就显然可以二分的。
那么问题出在哪里呢?其实二分出来的不一定要是答案,只要最后的这个东西能成为答案就行了,所以我确实不理解二分(
#include <cstdio>
const int N = 805;
int n, k, a[N][N], sum[N][N], flag[N][N], ans;
bool check(int x) {
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
flag[i][j] = a[i][j] <= x;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + flag[i][j];
for(int i = 1; i <= n-k+1; i++)
for(int j = 1; j <= n-k+1; j++) {
int d = sum[i+k-1][j+k-1] - sum[i-1][j+k-1] - sum[i+k-1][j-1] + sum[i-1][j-1];
if(d >= k*k-k*k/2) return true;
}
return false;
}
int main() {
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++) scanf("%d", &a[i][j]);
int l = 0, r = 1000000000;
while(l <= r) {
int mid = l + (r-l) / 2;
if(check(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
printf("%d", ans);
return 0;
}
这个比赛体验就顺利多了嘛!
加143,加航143 ?这谐音妙哇!
成功 PB, 而且成了历史上上分最快的一次。
顺利的提交。
甚至还达成了第一次提问,不过这个问题有点傻,晚上脑子比较糊涂。
"
rounded up
是上取整还是四舍五入?""举个例子,
5/2
rounded up 的结果是 ""那
10/7
是 还是 呢?"", 但是题目里只要去你除以二 😃"
直接模拟即可,因为有意义的变化显然不会超过 次。
保证输入的数据个数是偶数,所以若两个之间一定可以,那么 个也一定可以。
设第一个为 , 第二个为 , 然后搞一搞就可以发现一定可以。
核心代码。
for(int i = 1; i <= n; i += 2) {
printf("%d %d %d\n", 1, i, i+1);
printf("%d %d %d\n", 2, i, i+1);
printf("%d %d %d\n", 2, i, i+1);
printf("%d %d %d\n", 1, i, i+1);
printf("%d %d %d\n", 2, i, i+1);
printf("%d %d %d\n", 2, i, i+1);
}
搞个栈,若是 直接新建一层,若能接上直接把当前层加一,接不上就后退一层即可。
那么若有两种情况都满足要求怎么办?其实根本就不需要考虑这样的情况,因为你外面可以接下去,里面也一定可以接下去,直到接不下去的时候退回来不管怎么样都是要退的。
代码。
#include <cstdio>
const int N = 1005;
int T, n, x, st[N], top;
int main() {
scanf("%d", &T);
while(T--) {
top = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d", &x);
if(x == 1) st[++top] = 1;
else {
while(x != st[top] + 1) top--;
st[top] ++;
}
for(int j = 1; j < top; j++) printf("%d.", st[j]);
printf("%d\n", st[top]);
}
}
return 0;
}
D ……就不会了。
其实能上那么多分完全是运气好,因为 D 难度一下子增加的太大,导致区分度降低,我前面写得比较快,于是排名就高了,而且这又是一个 Div1 + Div2
, 所以分数就升得更多。
赛后解决 D.居然是随机化,不过是确保正确的随机化。
twt-tec
有 个产品。 个人,每个人喜欢twt-tec
的一些产品,最多不会喜欢超过 个,求最大的twt-tec
产品子集,使得有至少一半(上取整)的人喜欢这些产品。
看着这个数据范围就想着状压之类的东西,但是没有什么用。这子集肯定不能作状态,而不作状态又会有后效性。
然后注意到 这个条件,于是猜测最后的答案和输入的有一定的关系。但是赛场上我想到了答案可能是某一个人喜欢的,但这很快就被否决了,因为样例都过不了。
其实这就是关键点。
答案肯定是一个人喜欢的东西的子集。而题目又要求至少要有一半的人喜欢这些产品,所以我们随便选一个,就有 的概率选到包含答案的集合,把这个过程重复 次,没有选到包含答案的集合的概率就只有 , 这非常的小。按照官方题解的说法就是这个概率比你现在被陨石砸中的概率还要小
那么考虑缩小了范围后怎么找答案。可以枚举 个的子集,然后判断一下 个里面有多少人喜欢这个,然后来更新答案就可以了。
但是 是过不了的,所以不能直接去枚举。可以设置 表示喜欢的产品集合与 个组成的集合的交集为 的人有多少个。然后从小到大合并一下就可以了。
代码。一个很坑的地方是 rand()
的范围只到 , 所以要么使用极不均匀的 rand()*rand()
, 要么使用高科技的 mt19937
。后者要 c++11
, NOIP 不能用,所以我还是用了 rand()*rand()
。但这个似乎被 hack 伪随机数了。
#include <cstdio>
#include <ctime>
#include <cstdlib>
#include <vector>
const int N = 200005, M = 65, P = 16;
int n, m, p, sum[1 << P], max = -1;
char a[N][M], ans[M];
std::vector<int> t;
int popcount(int x) { return !x ? 0 : (popcount(x & (x-1)) + 1); }
int main() {
srand(time(0));
scanf("%d%d%d", &n, &m, &p);
for(int i = 1; i <= n; i++)
scanf("%s", a[i]);
for(int T = 1; T <= 20; T++) {
int u = rand()*rand() % n + 1;
t.clear();
for(int i = 0; i < m; i++)
if(a[u][i] == '1') t.push_back(i);
int p = t.size();
for(int i = 1; i <= n; i++) {
int v = 0;
for(int j = 0; j < p; j++)
if(a[i][t[j]] == '1') v |= 1 << j;
sum[v] ++;
}
for(int i = 0; i < p; i++)
for(int s = 0; s < (1 << p); s++)
if(!(s & (1 << i))) sum[s] += sum[s | (1 << i)];
for(int s = 0; s < (1 << p); s++)
if(sum[s] >= (n+1)/2 && popcount(s) > max) {
max = popcount(s);
for(int j = 0; j < m; j++) ans[j] = '0';
for(int j = 0; j < p; j++)
if(s & (1 << j)) ans[t[j]] = '1';
}
for(int i = 0; i < (1 << p); i++) sum[i] = 0;
}
printf("%s", ans);
return 0;
}
读 错 题 了 !
开始时 盏灯都是灭的,谭炜谭点灯大师会每次等概率随机选择一盏灭掉的灯点亮,求到第一次存在连续的一段 盏灯中被点亮了多盏时的期望亮灯数。
概率题,本质上是组合计数。
首先,若亮了 盏灯,一共有 种情况,那么每种情况出现的概率是 。剩下就是要求任意连续 个不存在多个球被选中的情况。情况数是
因为两个选出的相隔至少是 个,然后要有 个间隔,所以要在剩下的中选出 个来。
把两个东西乘起来就是选了 盏灯出现满足条件的情况的概率,对其求和可以得到期望。
代数推导感谢 @wxy_,在 这里。
其实就是把每步分开来处理,因为每次对答案的贡献都是 , 然后我们求的方案数就是有多少种方案活到而来这一步,而这每一个对答案的贡献都是 , 再除以每一个的概率,就是它们对期望的贡献。
好耶!被 zjk 叉掉了!
他叉掉了我的 D 题,代码如下:
#include <bits/stdc++.h>
const int N = 2e5 + 2;
int a[N], b[N], n;
int main() {
n = 2e5;
int yy = 1622449900;
for (int i = yy; i <= yy + 1000; i++) {
srand(i);
for (int i = 1; i <= 100; ++i) a[(rand() * rand()) % n] = 1;
}
printf("%d 2 2\n", n);
for (int i = 0; i < n; i++)
if (a[i]) puts("00");
else puts("11");
}
原理是这样的:因为数据规模是 , 而 time(0)
在一秒内只会有 个不同的种子出来,把这些种子所生成的随机数的位上全部变成 00
, 其它全部变成 11
, 就可以叉掉这 秒以内的提交,因为我们只会随机到这些 00
上。
同样,用 mt19937
也会遇上这个问题,所以得使用更高精度的时间作为种子,这样在一秒内有较大的不同种子才不会被叉掉。
#include <random>
#include <chrono>
std::mt19937 rnd(std::chrono::steady_clock::now().time_since_epoch().count());
记得开 c++11。
不过 ccf 的比赛里尽管用 time(0)
和 rand()
就好了,他不会设计数据来卡你,要是卡了,嘿嘿,那就好玩了。
sxyz 评测机卡 printf
。
不过在自己电脑上测 printf
也确实略慢,所以 以上输出量就上快写吧。
不开 long long
见祖宗。
【模板】单调栈
不交叉地将凸 边形切成 份的方案数。
显然,这有重叠子问题,可以用 dp 来做。
直接做就是 表示 边形分成 份,然后枚举加上去的一条边,, 但是这样子做显然会有重复,手模拟一下就会发现重复。
那么怎么处理呢,我就开始走歪路了。想着最外面的切割的个数就是一个方案会被重复计算的个数,而一个最外面的切割和新的拼起来有两种情况不会改变最外面的切割数,其它情况会使切割数小 , 所以加一维……
这样子做非常的复杂,而且很多细节难以确定,所以来看正解的做法。
正解先钦定了一个点,将没有经过其的计算出来,显然是 ,就是把这个点划出去或者不划出去。
这样子计算出来的方案肯定不会有重复,因为它没有转,下一层转是不会导致重复的,同时,这样子分割也可以确保在本层中和不会和经过该点的重复。
那么如何计算经过该点的呢?
其实非常简单,只需要枚举两个连接的点然后将左右分开来就可以了。没想到吧,处理旋转重复的最好方式就是不要旋转,因为左右计算的方案数里都包括了所有的方案,必然也就包括了旋转过去的,同时,这一条先的确定还能保证两边不重不漏。
挺妙。
定义
twt
距离为 , 输入 个点,求twt
距离第二大的。
肯定是最左边两个,最右边两个,最上面两个,最下面两个组合而成的,所以把这几个拉出来然后暴力跑一遍就可以了。
老板
twt
有 条狗,要去守门,twt-tec
有 道门。狗有三种颜色,每道门两只狗守,若颜色相同则不用花钱买饭喂狗,但是颜色不同就要花费 , 求twt-tec
的最小花费。
把三种颜色分开做,若三种颜色都有偶数个那么答案就是 了,否则一定是两个奇数个一个偶数个。
然后两种情况,一种是两个奇数组里面选出最接近的两个,另一种是偶数组中选出和两个奇数组中最接近的两个。
第一种很好写,第二种困扰了我很久,万一它们最接近的是同一个怎么办?最后找不出反例赌了一把直接不管这种情况居然过了。
其实确实不用管这样的情况,因为如果两个最接近的是同一个,那么这两个的差的绝对值肯定比再去找另外一个和这中间一个作差加上去要更优。
所以 遇到问题一定要证实问题存在。
掉分。
]]>这是一道二次离线的黑科技版题。
]]>这是一道二次离线的黑科技版题。
感觉现在去写这种黑科技意义不大,因为在做(卡)这题的时候不能通过自己的思考体会到算法的精妙,不过体验一下黑题也是不错的。这句话中的“卡”其实也有双关的意思
另外,这是 Ynoi 模拟赛,所以老板其实没有完成第一道 Ynoi(
有的时候用离线后用莫队做移动一个端点消耗的时间不是 的,所以总复杂度除了一个根号还得乘上那个操作的复杂度,没有办法接受。
所以可以把“移动”这一个操作再离线一遍,放到后面再一起做,使移动的复杂度均摊
求区间逆序对数。
首先有个简单的莫队做法,将询问离线,然后每次莫队移动的时候用个树状数组统计 比它大/比它小 的即变化量。时间复杂度大概是 。
分开仔细考虑一下左右指针移动时的变化量。
那么就记 是第 个中比 大的, 是前 个中比 小的。如果能快速处理这些东西,就可以快速计算上面的内容了。
把四种操作的增量重新写一遍。
发现其中的 和 可以方便地用树状数组求出来,然后考虑把剩下的离线下来处理。
可以从头到尾循环一遍,每次将 加入到某个数据结构中。可以发现照这个顺序,处理 移动的会在前面 个都处理出来的时候就得到答案,同理 移动要等到 出来的时候才得到答案,所以可以用 vector
将这些询问挂到对应的端点上,然后在循环的时候加(减)上相应的答案就可以了。
然后想想这个数据结构得是个啥。它需要支持 查询一个值域内数的个数(不然你白离线了), 但修改可以在 或者根号的时间内。
那么就上值域分块吧。
最后要注意我们记录的只是变化量,要做一个前缀和才是答案。
代码有点长,点击展开看。
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cmath>
#define re register
const int N = 100005, M = 350;
int a[N], c[N], l[M], r[M], s[M], sv[M][M], bv[N], blo[N], t[N],
n, m, size, B, ls[N], gr[N];
long long ans[N];
struct twt { int l, r, c, op, id; };
std::vector<twt> v[N];
struct dwd {
int l, r, id; long long ans;
bool operator < (dwd b) const {
return (blo[l] == blo[b.l]) ? ((blo[l]&1) ? (r < b.r) : (r > b.r)) : (blo[l] < blo[b.l]);
}
} q[N];
inline int ask(int x) { re int an = 0; for(re int i = x; i >= 1; i -= i&-i) an += t[i]; return an; }
inline void add(int x, int y) { for(re int i = x; i <= n; i += i &-i) t[i] += y; }
inline void update(int x) {
for(re int i = x-l[bv[x]], *k = sv[bv[x]]; i < size; i++) k[i] ++;
for(re int i = bv[x]; i <= bv[n]; i++) ++s[i];
}
inline int query(int x) { return s[bv[x]-1] + sv[bv[x]][x-l[bv[x]]];}
int main() {
cin >> n >> m;
for(re int i = 1; i <= n; i++) cin >> a[i], ls[i] = a[i];
std::sort(ls+1, ls+n+1);
for(re int i = 1; i <= n; i++) a[i] = std::lower_bound(ls+1, ls+n+1, a[i]) - ls;
for(re int i = 1; i <= n; i++) {
ls[i] = ask(a[i] - 1);
gr[i] = ask(n) - ask(a[i]);
add(a[i], 1);
}
size = sqrt(n);
for(re int i = 1; i <= n; i++) bv[i] = (i-1) / size + 1;
for(re int i = 0, j = 1; i < n; i += size, j++) l[j] = i+1, r[j-1] = i;
r[bv[n]] = n;
B = n / sqrt(m);
for(re int i = 1; i <= n; i++) blo[i] = (i-1) / B + 1;
for(re int i = 1; i <= m; i++) cin >> q[i].l >> q[i].r, q[i].id = i;
std::sort(q+1, q+m+1);
for(re int i = 1, l = 1, r = 0; i <= m; i++) {
if(l > q[i].l) v[r].push_back((twt){q[i].l, l-1, 1, 1, i});
while(l > q[i].l) q[i].ans -= ls[--l];
if(r < q[i].r) v[l-1].push_back((twt){r+1, q[i].r, 0, -1, i});
while(r < q[i].r) q[i].ans += gr[++r];
if(l < q[i].l) v[r].push_back((twt){l, q[i].l-1, 1, -1, i});
while(l < q[i].l) q[i].ans += ls[l++];
if(r > q[i].r) v[l-1].push_back((twt){q[i].r+1, r, 0, 1, i});
while(r > q[i].r) q[i].ans -= gr[r--];
}
for(re int i = 1; i <= n; i++) {
update(a[i]);
for(re int j = 0; j < (signed)v[i].size(); j++) {
twt x = v[i][j];
int l = x.l, r = x.r, c = x.c, op = x.op, id = x.id;
for(int k = l; k <= r; k++)
if(c == 1) q[id].ans += op * query(a[k]-1);
else q[id].ans += op * (query(n) - query(a[k]));
}
}
for(re int i = 2; i <= m; i++) q[i].ans += q[i-1].ans;
for(re int i = 1; i <= m; i++) ans[q[i].id] = q[i].ans;
for(re int i = 1; i <= m; i++) cout << ans[i] << '\n';
return 0;
}
这题卡常卡了好久,发现奇偶性优化还是挺管用的。
放个重载好的快读快写吧。
namespace IO {
#define BUFSIZE 10000000
struct read {
char buf[BUFSIZE], *p1, *p2, c, f;
read() : p1(buf), p2(buf) {}
char gc(void) {
if (p1 == p2) p2 = buf + fread(p1 = buf, 1, BUFSIZE, stdin);
if (p1 == p2)
return EOF;
else
return *p1++;
}
read& operator>>(int& x) {
c = gc(), f = 1, x = 0;
for (; c < '0' || c > '9'; c = gc())
if (c == '-') f = -1;
for (; c >= '0' && c <= '9'; c = gc()) x = x * 10 + c - '0';
x *= f;
return *this;
}
};
struct write {
char buf[BUFSIZE], *p1, *p2, s[50];
int tp;
write() : p1(buf), p2(buf + BUFSIZE) {}
~write() { flush(); }
void flush(void) {
fwrite(buf, 1, p1 - buf, stdout);
p1 = buf;
}
void pc(char c) {
if (p1 == p2) flush();
*p1++ = c;
}
write& operator<<(long long x) {
if (x < 0) x = -x, pc('-');
do {
s[tp++] = x % 10 + '0', x /= 10;
} while (x);
while (tp) pc(s[--tp]);
return *this;
}
write& operator<<(char x) {
pc(x);
return *this;
}
};
read cin;
write cout;
}
using IO::cin;
using IO::cout;
APIO 大概是算正式比赛(?)了吧,所以这似乎不是事故征候调查报告了,而是事故调查报告了——不过这种情况在 OI 赛制中不大会遇到。
APIO 大概是算正式比赛(?)了吧,所以这似乎不是事故征候调查报告了,而是事故调查报告了——不过这种情况在 OI 赛制中不大会遇到。
开始听课了……听不懂,跑了。
听不懂 2
《不会有人掉线的》
《掉线了可以马上重连》
前面还撑了一会儿,后面开始黑题多了就又听不懂了,跑了。
不过多了两发通过。
上午来机房写了主席树/带修主席树的板子,就等待开始比赛。
没想到就在原机房里比赛,还以为要换机房以方便视频监考呢(
A 很快读完了题面,运用出色的初中平面几何能力,很容易发现 A 的 的点就是等边三角形,很快推出了式子。
没急着写,反正就两三行,看了下下面的 subtask, 觉得暴力可做,但暴力太难写了,于是放弃,开始写 分。
然后就开始走向事故了,交了一发 WA, 又交了一发 WA, 仔细读题,原来是 和 搞反了,好吧好吧,这会儿该对了,结果一交继续 WA,赛场上就很心态爆炸。一个小时居然一分没得。继续好好读题——发现边长似乎是 , 然后一改就过了,于是一个小时半只得了 分。
B 题看着很眼熟,有点像线段树优化建图,于是我打开了洛谷,然后再把题目好好读。(押韵)
前面的 subtask 似乎单调栈建边都不用,直接 floyd 做就完了。后面也不大可能是比较普适的图论问题,不然百度一下就会有了, 所以想着挖掘一下性质。然后 nottttttthy 和 墨神 就在旁边叽里呱啦讲起了性质,但是我不想被剧透,于是把他俩请到外面去了。
结果自己研究了半天啥性质也没有发现,时间复杂度没有得到任何优化。而且当时脑子混沌一片,连多源 bfs 这种显然的东西都没有想到,一直想着离线下来一起考虑可能会减少很多的重复和不必要……
想什么呢!这不是强制在线吗!
想了会儿没啥思路,隔壁小渔已经宣布他在 C 题获得了 分的优异成绩了,于是去想 C。
显然第一个 分排个序就行了。
第二个 分想了会儿贪心然后及时悬崖勒马了,发现是一个很简单的 dp ,于是写了就交了,然后进入一直 waiting 状态。
虽然说 APIO 要好好打,但是饭还是得吃的。
吃饭的时候交流了一下发现我现在 的得分居然挺高的了。
吃完饭测完了……居然 WA 了。
这么简单的 dp 我能 WA ? 找渔一问发现做法是对的,然后盲猜是边界有问题,修改了一波边界就交了。
再次看到 B 的时候发现无权,所以直接多源 bfs 就可以 分了,于是写了个交上,然后发现评测机已经 SPFA 了。
也不太想做测试,就观察了一下代码,觉得这么简单的程序应该不会写错的(刚刚写挂了 C 的 分忘了?), 然后想了一下下一个点怎么做。
尝试用数据结构大力维护,可是未果。
继续挖掘性质也没发现什么有用的。其实是人比较浮躁了没法静下心来了。索性就不写了。
看 C
发现 的数据似乎可以 dp, 但是觉得方程可能会有些小问题,结合当时的情况觉得银牌应该没戏了,所以也不想做了,弃疗。
C 居然是原题。惊了。
赛后讲分块也不想听。不过 nottttttthy 倒是对女选手讲课很感兴趣。
放学前打了下篮球然后成功地受伤了,严重的萝卜干,手指都肿了,用点力弯曲伸直就痛,回到家后冰敷了也没用,第二天变得青一块紫一块,样子十分恐怖。
篮球太危险了,再也不大篮球了。
晚上 ABC PB 了,开心。
第二天 ARC 没有什么有价值的思考出来的成果,分数原地不动。
居然有讲题?居然有颁奖典礼?
电脑上下 zoom 怎么那么的慢,用 wget
也很慢。
手机里放没有声音,所以不看了。
据说铜牌线是 , 银牌只有 。
什么!银牌只有 , 那银牌完全可以得啊!这个时候有些后悔弃疗了。
收到了 CCF 发来的邮件。
果然挂分。不过若果得了 恐怕感觉会更糟糕,只差三分就上银了。
% 不挂分选手 LYC!
然后开始调查事故原因
首先打开 C 的存档,检查哪里挂了。
一眼就看见没开 long long
。
好了,调查结束。
!!!!!!!!!
因为赛制不同所以全程未执行检查单,导致事故发生。
按照 CAAC 的风格恐怕是要“暂停 #define int long long
在算法竞赛中的应用”。
因为对比赛结果失去信息所以放弃治疗。
改进方式:
其实 APIO 的失败主要还是在于平时的训练吧,说着冲刺 APIO, 可是简要日志里面基于 SOP SCR CI 的练习情况评价全是 Failed。
APIO 后,吸取教训,再出发。
]]>晚上状态挺好,一下就恢复信心了。
]]>晚上状态挺好,一下就恢复信心了。
D 一下子没有想清楚,吃了一发罚时,其它都还是可以的。
手指受伤前两题慢了些,对后面的题应该影响不大。
水题,直接 减掉输入的仨。
照题目说的做就行了。
拿个桶先存一下 B 中出现的就可以了。
求 个
a
和 个b
组成的字符串中字典序第 小的。
思路倒是很快有,就是每次确定最前面的一个 b
的位置,然后一个个处理下去。但是细节上有点小问题,导致我调了一会儿。
这是错误代码:
for(int i = 1; i <= B; i++) {
int j;
for(j = n-b+1; j > B-b+1 && C[n-j+1][b-1] < k; j--);
flag[j] = 1;
k -= C[n-j][b-1];
b--;
}
我们要确认的第一个的位置应该是第一个随便排会大于等于现在的 的位置,而不是确定了这个后面随便排要小于 的位置,然后再 减掉后面随便排的方案, 个数去掉 继续做就可以了。
这才是正确代码:
for(int i = 1; i <= B; i++) {
int j;
for(j = n-b+1; j > B-b+1 && C[n-j+1][b] < k; j--);
flag[j] = 1;
k -= C[n-j][b];
b--;
}
输入一棵树, 次询问,每次求 的子树中深度为 的点的个数。
直接做 显然会超时。
模拟下这个过程发现每次找了其它的深度是完全没有必要的,所以把每个深度分开来存储,然后考虑如何快速判断一个深度的点有几个在 的子树里。
判断是否在子树里很容易想到 dfs 序,只要看 dfs 序是不是在 这个区间里就可以了。为了快速做到这一点,可以将每一个深度的点按 dfs 序排序,然后再 std::lower_bound
一下起止点就可以了。
代码。
#include <cstdio>
#include <vector>
#include <algorithm>
const int N = 200005;
int n, x, y, m, dfn[N], size[N], tot;
std::vector<int> g[N], a[N];
void dfs(int u, int fa, int d) {
size[u] = 1;
dfn[u] = ++tot;
a[d].push_back(dfn[u]);
for(int i = 0; i < (signed)g[u].size(); i++) {
int v = g[u][i];
if(v == fa) continue;
dfs(v, u, d+1);
size[u] += size[v];
}
}
int main() {
scanf("%d", &n);
for(int i = 2; i <= n; i++) {
scanf("%d", &x);
g[x].push_back(i), g[i].push_back(x);
}
dfs(1, 0, 0);
for(int i = 0; i < n; i++)
if(a[i].size()) std::sort(a[i].begin(), a[i].end());
scanf("%d", &m);
while(m--) {
scanf("%d%d", &x, &y);
int l = dfn[x], r = dfn[x] + size[x] - 1;
int L = std::lower_bound(a[y].begin(), a[y].end(), l) - a[y].begin(),
R = std::upper_bound(a[y].begin(), a[y].end(), r) - a[y].begin();
printf("%d\n", std::max(R-L, 0));
}
return 0;
}
难得在一个小时多一点就 A 了 E 题。
计算几何题,而且很难,没思路,总共才 个人过。
APIO 报名时的豪迈壮志最终一天天的化解, 天一过仍未有任何进步。每天浑浑噩噩的都过去了。
林语堂说:
一个人心地干净,思路清晰,没有多余情绪和妄想的人,是会带给人安全感的,因为他不伤人,也不自伤,不制造麻烦,也不麻烦别人。某种程度上来说,这是一种持戒。
翘翘所谓的可爱,只是在原来可爱优秀的同学们中受感染的罢了。只有真正的独立的做到和坚持“心地干净,思路清晰,没有多余情绪和妄想”,才是真正可爱的翘翘。
任何时候改变都来的及,前方的路还长。加油。
]]>