【Coel.题解合集】【最终之落笔】珂艾尔个人题目的题解合集!
年到了,给大家送点新年礼物,顺便给咱的 OI 旅程画上圆满句号w
下面的题解按照题单的顺序排列,如果找不到可以直接用 Ctrl+F
搜索标题喵w
首先是 CJOI (咱的个人赛!)的题目,这些内容全部复制粘贴自这里,懒得再写一次了(
[CJOI-Ex]一道简单的英文题。
这是一个脑筋急转弯。
一般人直接把描述全部翻译完了,就会觉得:“woc 我又不会读心术怎么知道你想的数字是多少?”
(虽然咱希望能有一个和珂艾尔心灵相通的人出现)
但是请注意题目背景:选择性地翻译题面并理解题目含义。据此我们可以得到真正的翻译结果:
题目描述:珂艾尔在心里想了一个数,请输出 the number that Coel thought.
输入格式:本题没有输入。
输出格式:输出只有一行,为 the number that Coel thought.
说明/提示:本题没有数据范围。
因此,只需要输出the number that Coel thought.
就可以了。
int main() {
puts("the number that Coel thought.");
return 0;
}
[GLFLS 5A][CJOI-A]诈骗大师
本题同时作为国龙模拟赛 A 题。
这道题的标准写法是用单调队列,然而在下造数据能力有限,随便怎么写都可以过,所以不写思维过程了。
int main(void) {
cin >> n >> m;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= m; i++)
cin >> s[i].l >> s[i].r;
for (int i = 1, j = 1; i <= n; i++) {
while (j <= m && s[j].l <= a[i])
j++;
j--;
if (s[j].r >= a[i] && s[j].l <= a[i] && !vis[j]) {
ans.push_back(s[j]);
vis[j] = true;
}
}
cout << ans.size() << endl;
for (auto v : ans)
cout << v.l << ' ' << v.r << endl;
return 0;
}
[CJOI-B]快速公约数变换
本题灵感来源于知乎上的某个问题,虽然现在找不到了。
因为 和 相等,而 只会出现一次,所以所有出现次数为奇数的数字就是答案,开一个数组 存放数字 的出现次数,然后从大到小把答案输出一遍。
int main(void) {
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1, x; j <= n; j++) {
cin >> x, b[x]++;
maxx = max(maxx, x);
}
for (int i = maxx; i >= 1; i--)
if (b[i] & 1) cout << i << ' ';
return 0;
}
本题其实是CF582A 的弱化,这个做法仅限于所有 互不相同,如果相同就要用另外的做法了。
[CJOI-C]对 1 的渴求
个人觉得是一个比较“思维好题”的题,然而自己的解法被 Jeslan 的爆了,有点尴尬。
首先,如果数列中有一个 ,由于 ,所以让这个 每次和相邻的 合并即可,操作次数为 。
如果 有 个,那么让 和相邻不是 的数合并即可,操作次数为 ,只写这个特判可以得 分。
如果不存在 ,则应当尽可能快速地合并出一个 来,再按照上面的方法求解。那么,问题转化为找到一个范围尽可能小的区间 ,使得 。
暴力枚举左右端点求区间 的做法是 ( 为值域) 的,可以得 分。
注意到固定左端点后,右端点的增加会使得区间 具有单调性,因此考虑二分,只枚举左端点,对每个枚举到的左端点二分查找右端点的位置,同时维护区间长度最小值。
此时我们需要一个快速求解区间 的数据结构,用 ST 表或者线段树均可。
另外可以发现,这个区间其实可以用双指针维护,这样时间复杂度可以降到 ,但本题二分已经能过了,所以没必要再优化。
void initST() {
for (int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
for (int i = 1; i <= n; i++) ST[i][0] = a[i];
for (int j = 1; (1 << j) <= n; j++)
for (int i = 1; i <= n - (1 << j) + 1; i++)
ST[i][j] = __gcd(ST[i][j - 1], ST[i + (1 << (j - 1))][j - 1]);
}
int query(int l, int r) {
int t = lg[r - l + 1];
return __gcd(ST[l][t], ST[r - (1 << t) + 1][t]);
}
signed main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
if (a[i] == 1) cnt++;
}
initST();
if (cnt != 0) cout << n - cnt, exit(0); //数列存在 1,答案为 n 减去 1 的个数
if (query(1, n) != 1) cout << "Unhealthy!", exit(0); //无法凑出 1,无解
for (int i = 1; i <= n; i++) {
int l = i + 1, r = n;
while (l <= r) { //二分过程
int mid = (l + r) >> 1;
if (query(i, mid) == 1) {
res = min(res, mid - i);
r = mid - 1;
} else l = mid + 1;
}
}
cout << res + n - 1;
return 0;
}
[CJOI-D]最初的贤者
顺带一提,这场比赛出在原神更新散兵周本之后的两个星期以内。
再顺带一提,我喜欢纳西妲。
这道题其实是一个类似最长上升子序列的动态规划。设 表示最后摧毁的灭度机为第 个时能够得到的最大能量,显然状态转移方程为
这样写的时间复杂度为 ,可以得到 分。
考虑进行优化。回想一下导弹拦截那道题,我们利用了一个单调栈将其优化到了 。这里当然也是做转移优化,不过用的不是单调栈,而是 CDQ 分治。
事实上,导弹拦截本质上是一个二维偏序问题,而 CDQ 分治适用于解决三维偏序的问题。而本题有四个维度,但做法还是一样的,思想就是维度的消除。第一维,排序;第二维,做 CDQ 分治。第三维呢?再套一个 CDQ 分治!最后第四维写一个树状数组,大功告成。
需要注意的是,里层 CDQ 分治计算贡献时,必然是外层 CDQ 分治的右半区间给左半区间求贡献。因此,做外层 CDQ 分治时要顺带着把区间划分给记录下来,对每个点开一个 表示这个点在外层分治的左半区间还是右半区间。
这道题的树状数组是用来求区间最值的,所以修改操作要改成求最大值的操作。另外本题数据有负数,所以树状数组的初始化要设置为负无穷。
套了两层 CDQ 分治,再加上内层的树状数组,时间复杂度为 。当然这题还有很多别的解法,比如 CDQ 分治和树套树的嵌套,K-D Tree,Bitset 等等,理论上能求三维偏序的都可以拿来做这道题。
const int maxn = 5e4 + 10;
const ll inf = 1e18;
int n;
int d[maxn], dtop, atop;
ll ans = -inf, dp[maxn];
struct node {
int a, b, c, d, id;
ll sum, val;
bool vis;
inline bool operator==(const node &x) {
return a == x.a && b == x.b && c == x.c && d == x.d;
}
} a[maxn], t1[maxn], t2[maxn];
template<typename T> void gma(T &x, T y) { if (x < y) x = y; }
class Fenwick_Tree { //树状数组
private:
#define lowbit(x) (x & (-x))
ll c[maxn];
public:
void init() {
memset(c, -0x3f, sizeof(c));
}
void add(int x, ll v) {
for (int i = x; i < maxn; i += lowbit(i)) gma(c[i], v);
}
ll query(int x) {
ll res = -inf;
for (int i = x; i; i -= lowbit(i)) gma(res, c[i]);
return res;
}
void toZero(int x) {
for (int i = x; i < maxn; i += lowbit(i)) c[i] = -inf;
}
} T;
bool cmp1(node x, node y) { //第一维排序
if (x.a != y.a) return x.a < y.a;
if (x.b != y.b) return x.b < y.b;
if (x.c != y.c) return x.c < y.c;
return x.d < y.d;
}
bool cmp2(node x, node y) { //第二维排序
if (x.b != y.b) return x.b < y.b;
if (x.c != y.c) return x.c < y.c;
if (x.d != y.d) return x.d < y.d;
return x.a < y.a;
}
bool cmp3(node x, node y) { //第三维排序
if (x.c != y.c) return x.c < y.c;
if (x.d != y.d) return x.d < y.d;
if (x.a != y.a) return x.a < y.a;
return x.b < y.b;
}
void init_hash() {
sort(d + 1, d + n + 1);
dtop = unique(d + 1, d + n + 1) - d - 1;
for (int i = 1; i <= n; i++)
a[i].d = lower_bound(d + 1, d + dtop + 1, a[i].d) - d;
}
void CDQ_inside(int l, int r) { //内层 CDQ 分治
if (l == r) return;
int mid = (l + r) >> 1;
CDQ_inside(l, mid);
for (int i = l; i <= r; i++) t2[i] = t1[i];
sort(t2 + l, t2 + mid + 1, cmp3);
sort(t2 + mid + 1, t2 + r + 1, cmp3);
for (int i = mid + 1, j = l; i <= r; i++) {
while (j <= mid && t2[i].c >= t2[j].c) { //计算左边对右边影响
if (t2[j].vis == false) T.add(t2[j].d, dp[t2[j].id]);
j++;
}
if (t2[i].vis == true) gma(dp[t2[i].id], T.query(t2[i].d) + t2[i].val);
}
for (int i = l; i <= mid; i++)
if (t2[i].vis == false) T.toZero(t2[i].d); //还原树状数组
CDQ_inside(mid + 1, r);
}
void CDQ_Divide(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
CDQ_Divide(l, mid);
for (int i = l; i <= r; i++) t1[i] = a[i];
for (int i = mid; i <= r; i++) t1[i].vis = true; //右半区间记为 true
sort(t1 + l, t1 + r + 1, cmp2);
CDQ_inside(l, r);
CDQ_Divide(mid + 1, r);
}
int main(void) {
T.init();
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].a >> a[i].b >> a[i].c >> a[i].d >> a[i].val;
a[i].id = i;
a[i].c *= -1; //乘个 -1 相当于把符号颠倒一下
d[i] = a[i].d;
}
init_hash(); //离散化第四维,方便树状数组的操作
sort(a + 1, a + n + 1, cmp1);
for (int i = 1; i <= n; i++) { //对属性相同点去重(只保留能量为正数的点)
if (a[i] == a[i - 1]) {
if (a[i].val > 0) a[atop].val += a[i].val;
} else a[++atop] = a[i], a[atop].id = atop;
}
for (int i = 1; i <= n; i++) dp[i] = a[i].val; //dp 边界处理
n = atop;
CDQ_Divide(1, n);
for (int i = 1; i <= n; i++) gma(ans, dp[i]);
cout << ans << '\n';
return 0;
}
接下来是出在其他模拟赛的散题。
[GLFLS 4C]高考数学
莫比乌斯反演的模板题。模板也能被放在模拟赛里面,谢谢 Sherlockk 给的面子
算分母是比较简单的过程,所以先讲一下。
设 到 可选数字个数为 ,可以知道 ,那么分母就是 。
接下来推推式子:
这样就能以 的复杂度求出分母。顺带一提,如果你在物理学到了玻尔原子模型,会发现 十分常用。
下面是求分子。不难将其表示为
由于 的起始值不定,很难操作,所以改写为
运用一点简单的反演知识,可以得到
结合数论分块即可求出答案。代码比较简单,也就不放了。
[GLFLS 6φ] 去他的物理实验
分部分分讲解。
20 分做法
我会待定系数!
由于 ,也就是说能够使所有点落在同一条直线上,所以我们只需要使用待定系数法:任意选择两个给定的点,代入到 中解出 和 即可。
方法很简单,这里就不放参考代码了。
60 分做法
我会三分!
注意到 和 的取值都会导致 发生变化,而且答案具有单峰性(类似二次函数,顶点的两边都具有单调性),所以我们可以考虑使用三分法,且由于有两个变量,使用三分套三分。
这里实际上利用了一个性质:当 为定值时,我们可以把 看做是关于 的函数,且 的最值点左边单调递减,右边单调递增;当 为定值时,也有同样的结果。
具体来说,我们在外层的三分控制 的取值,在内层的三分控制 的取值即可。下面是 Jeslan 提供的代码:
#include <iostream>
#include <cstring>
const int MAXN = 1e6+5;
int n;
double x[MAXN], y[MAXN];
double getQ(double k, double b) { //当前求得的 k 与 b 对应计算出 Q
double ret = 0;
for(int i=1; i<=n; ++i) {
double temp = x[i] * k + b - y[i];
ret += temp * temp;
}
return ret;
}
double getB(double k) { //当前求得的 k 对应计算出 b,进行内层三分
double bL = -1e4, bR = 1e4;
while(bR - bL > 1e-4) {
double bML = bL + (bR - bL) / 3.0;
double bMR = bR - (bR - bL) / 3.0;
double fL = getQ(k, bML);
double fR = getQ(k, bMR);
if(fL <= fR) bR = bMR;
if(fL >= fR) bL = bML;
}
return bL;
}
int main() {
scanf("%d", &n);
for(int i=1; i<=n; ++i) {
scanf("%lf %lf", x+i, y+i);
}
double kL = -1e4, kR = 1e4;
while(kR - kL > 1e-4) { //外层三分
double kML = kL + (kR - kL) / 3.0;
double kMR = kR - (kR - kL) / 3.0;
double fL = getQ(kML, getB(kML));
double fR = getQ(kMR, getB(kMR));
if(fL <= fR) kR = kMR;
if(fL >= fR) kL = kML;
}
printf("%.3f %.3f", kL, getB(kL));
return 0;
}
该方法的时间复杂度为 ( 为值域),已经是很优秀的做法了。
满分做法
我是 whk 高手!
本题实际上考察的是一元线性回归方程,考虑到赛时选手都没学过,所以用来考验一下大家的推式子能力。
推导过程在各位的数学课本上都有,就不直接写了。
结论为
代入求解即可。
int main(void) {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> x[i] >> y[i];
ave_X += x[i], ave_Y += y[i];
}
ave_X /= n, ave_Y /= n;
for (int i = 1; i <= n; i++) {
up += (x[i] - ave_X) * (y[i] - ave_Y); //求 k 的分子
dn += (x[i] - ave_X) * (x[i] - ave_X); // 求 k 的分母
}
k = up / dn, b = ave_Y - k * ave_X;
cout << fixed << setprecision(5) << k << ' ' << b;
return 0;
}
[GLFLS 7F] 阿洛娜导航
玩 BA 玩的。
本来想写一个小剧场来解释,然而时间不够。
几个特殊点都是送分的,不解释,直接看 。
这个数据范围是标准的单源最短路算法,考虑做转化。借用网络流中“拆点”的原理,我们可以把代表报名费的点权转换成边权。
具体怎么做?假想有一个点编号 ,让每个点都和它相连,权值等于报名费。这个时候,要求第 个点的最小花费,只需求出该点到 号点的最短路即可。
这时求的是多源点、单汇点的最短路,考虑对称性,可以反过来求 号点到其他点的最短路,这样就能划归到单源最短路径上了。
后面两个 分点是用来卡 SPFA 的,用 Dijkstra 就可以了,当然如果你有高级的 SPFA 优化算法也可以用。
int main(void) {
cin >> n >> m;
memset(head, -1, sizeof(head));
for (int i = 1, x; i <= n; i++) {
cin >> x;
add(0, i, x); //连接 0 号点
}
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
add(v, u, w);
}
dijkstra(0);
for (int i = 1; i <= n; i++)
cout << dis[i] << ' ';
return 0;
}
本文作者:Coel's Blog
本文链接:https://www.cnblogs.com/Coel-Flannette/articles/17939367
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步