abc339 详解
第一篇整场题解纪念我第一次 AK 的 abc!
A
#include <iostream>
using namespace std;
int main(int argc, const char * argv[]) {
string str;
cin >> str;
int len = (int)str.length();
string out;
for (int i = len - 1; i >= 0; --i) {
if (str[i] == '.') {
break;
}
out += str[i];
}
reverse(out.begin(), out.end());
cout << out << endl;
return 0;
}
B
用 ways 数组和 way 变量控制方向是一个快速模拟技巧
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e2 + 1;
const int ways[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
char str[N][N];
int a[N];
int main(int argc, const char * argv[]) {
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
memset(str, '.', sizeof(str));
int x = 1, y = 1, way = 0;
while (k--) {
if (str[x][y] == '.') {
str[x][y] = '#';
(++way) %= 4;
} else {
str[x][y] = '.';
((--way) += 4) %= 4;
}
x += ways[way][0]; y += ways[way][1];
if (y == 0) {
y = m;
}
if (x == 0) {
x = n;
}
if (y == m + 1) {
y = 1;
}
if (x == n + 1) {
x = 1;
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
putchar(str[i][j]);
}
putchar('\n');
}
return 0;
}
C
注意到一开始可能的最小的在车上的人数就是 a 数组前缀和数组中的最小值的相反数和 0 的较大值。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N = 2e5 + 1;
ll s[N];
int main(int argc, const char * argv[]) {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
s[i] = s[i - 1] + x;
}
printf("%lld\n", max(0ll, -*min_element(s + 1, s + n + 1)) + s[n]);
return 0;
}
D
思路
一看到最短路,就想到 bfs。那有两个人,怎么 bfs 呢?最暴力的想法就是记 \(dis[x_1][y_1][x_2][y_2]\) 表示初始状态到第一个玩家在 {\(x_1\), \(y_1\)}, 第二个玩家在 {\(x_2\), \(y_2\)} 的最短路。每次枚举四个方向两个玩家尝试一起走,如果有一个玩家面前不能走留在原地即可。最后答案枚举 \(i\) 和 \(j\),取 \(dis[i][j][i][j]\) 中的最小值即可。时空复杂度 \(O(n^4)\)。
实现
提供我赛时简洁实现
#include <iostream>
#include <cstring>
#include <climits>
#include <queue>
using namespace std;
struct Pos {
int x, y;
bool operator == (const Pos &p) const {
return x == p.x && y == p.y;
}
Pos operator + (const int a[2]) const {
Pos hero = *this;
hero.x += a[0]; hero.y += a[1];
return hero;
}
void operator += (const int a[2]) {
*this = *this + a;
}
};
typedef pair<Pos, Pos> ppp;
const int N = 61;
const int ways[4][2] = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
char str[N][N + 1];
int dis[N][N][N][N];
int n;
inline bool inArea(const Pos &p) {
return p.x >= 1 && p.x <= n && p.y >= 1 && p.y <= n;
}
int bfs(const Pos &p1, const Pos &p2) {
memset(dis, 127, sizeof(dis));
dis[p1.x][p1.y][p2.x][p2.y] = 0;
queue<ppp> que;
que.push({p1, p2});
while (!que.empty()) {
Pos nowP1 = que.front().first, nowP2 = que.front().second; que.pop();
int &nowDis = dis[nowP1.x][nowP1.y][nowP2.x][nowP2.y];
for (int i = 0; i < 4; ++i) {
Pos newP1 = nowP1; newP1 += ways[i];
if (!inArea(newP1) || str[newP1.x][newP1.y] == '#') {
newP1 = nowP1;
}
Pos newP2 = nowP2; newP2 += ways[i];
if (!inArea(newP2) || str[newP2.x][newP2.y] == '#') {
newP2 = nowP2;
}
int &newDis = dis[newP1.x][newP1.y][newP2.x][newP2.y];
if (nowDis + 1 < newDis) {
newDis = nowDis + 1;
que.push({newP1, newP2});
}
}
}
int ans = 1 << 30;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
ans = min(ans, dis[i][j][i][j]);
}
}
}
return ans < 1 << 30 ? ans : -1;
}
int main(int argc, const char * argv[]) {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%s", str[i] + 1);
}
vector<Pos> ps;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (str[i][j] == 'P') {
ps.push_back({i, j});
}
}
}
Pos p1 = ps.front(), p2 = ps.back();
printf("%d\n", bfs(p1, p2));
return 0;
}
E
思路
首先考虑 \(O(n^2)\) dp。设 \(f[i]\) 以 \(i\) 结尾的最长合法子序列的长度。于是很自然列出转移方程为 \(f[i] = max\{[i < j \wedge \lvert a[i] - a[j] \rvert \le d] \times f[j] + 1\}\)。接下来我们就想怎么优化这个式子。
诶也先别急着一步优化成复杂度正确。我们想一下,如果它是一道 OI 题,出题人是不是有可能放一个值域做法部分分?我们尝试思考一下 \(O(nd)\) 的 dp 怎么做。我们在从左往右做的过程中,设 \(g[i]\) 为使得 \(a[p] = i\) 的 \(f[p]\) 的最大值,那么 \(f[i] = max([j\in[a[i] - d, a[i] + d]] \times g[j] + 1)\),之后再用 \(f[i]\) 试着更新 \(g[a[i]]\) 即可。但因为如果序列中有两个数相同,那么后出现的一定能拼在前面那个数的后面,所以必定更优,直接赋值即可。从左往右做的过程中自然保证了 \(i < j\),所以限制条件都满足,正确。时间复杂度为 \(O(nd)\)。
我们再想一下 \(O(nd)\) 中解法我们在干一件什么事。\(f[i] = max([j\in[a[i] - d, a[i] + d]] \times g[j] + 1)\),也就是查询 \(g\) 一段区间中的最大值。\(g[a[i]] = f[i]\),也就是单点修改。
区间最大值,单点修改,我们想到什么?没错!这道题现在就被我们转换成了一道比较基础的线段树运用来优化 dp,lazytag 都用不到。每次查询一次最大值再单点修改,时间复杂度 \(O(n \log d)\)。
代码
提供一个很简单的实现
#include <iostream>
using namespace std;
const int V = 5e5;
int tree[(V + 1) << 2];
inline void pushup(int pos) {
tree[pos] = max(tree[pos << 1], tree[pos << 1 | 1]);
}
void pointModify(int x, int v, int l = 1, int r = V, int pos = 1) {
if (l == r) {
tree[pos] = max(tree[pos], v);
return ;
}
int mid = (l + r) >> 1;
if (x <= mid) {
pointModify(x, v, l, mid, pos << 1);
} else {
pointModify(x, v, mid + 1, r, pos << 1 | 1);
}
pushup(pos);
}
int rangeMax(int x, int y, int l = 1, int r = V, int pos = 1) {
if (x <= l && r <= y) {
return tree[pos];
}
int res = 0;
int mid = (l + r) >> 1;
if (x <= mid) {
res = max(res, rangeMax(x, y, l, mid, pos << 1));
}
if (y >= mid + 1) {
res = max(res, rangeMax(x, y, mid + 1, r, pos << 1 | 1));
}
return res;
}
int main(int argc, const char * argv[]) {
int n, d;
scanf("%d%d", &n, &d);
for (int i = 1; i <= n; ++i) {
int x;
scanf("%d", &x);
pointModify(x, 1 + rangeMax(max(1, x - d), min(x + d, V)));//实际上用不到 a 和 dp 数组,线段树上面就存了以前状态的信息
}
printf("%d\n", rangeMax(1, V));
return 0;
}
F
思路
值域实在太大了,想要用调和级数之类的预处理肯定是不行的,所以我们只能在判定上做文章。注意到 \(n = 10^3\),也就是说枚举二元组是允许的。如果说枚举每对相乘 \(O(n^3)\) 在 atc 神机上还能卡一卡,那么开一个桶存每个乘积出现次数是无法避免空间爆炸的。
看似没有任何办法了,但我们可以这样想:既然每次回答一个二元组乘积是否等于一个数,要做到 100% 正确是很难的,但是我们如果稍微放宽一点要求,做到 99.999999999% 正确,那么至少在这道题上面是很够用的,而且做法会简单很多。
于是我们可以自然地想到哈希的思想。你可能说,这也可以哈希?但别急,先听。我们选定一个模数 \(P\),则 \((a\times b) \% P = c \% P\) 是 \(a \times b = c\) 的必要不充分条件。如果有 \(a \times b = c\),那么一定有 \((a \times b) \% P = c \% P\),反之亦然:如果没有 \((a \times b) \% P = c \% P\),也一定没有 \(a \times b = c\)。这是必要性。但是没有充分性,也就是如果有 \((a \times b) \% P = c \% P\),那么不一定有 \(a \times b = c\),反之亦然。哈希就是把难判定充分必要的问题转换成简单极高概率判定必要但是不充分的问题的思想,并不只是作用于字符串或者哈希表的运用上。
如果此题仍然使用 \(10^9\) 级别的素数,正确性显然是不够的。此题因为是对乘积做哈希,如果不对着卡在模意义下分布是较为均匀的。\(\frac{10^6}{10^9}\),碰撞率是不是看起来就很紧张。所以可以扩大模数规模或者使用多哈希。推荐使用多哈希,因为这样可以避免对着一个特定素数卡,所以这道题取 3 个 \(10^9\) 级别的素数或者 2 个 \(10^{18}\) 的素数是较为保险的。在打 CF 的时候为了避免被模数对着 hack,更推荐使用随机而不是固定模数。
实现
赛时用了 \(10^{18} + 3\) 当模数单哈希就过了,这里提供一种写法优美双哈希的实现。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef unsigned long long ull;
const int N = 1e3 + 1;
const ull p1 = 200820252011, p2 = 201020080601;
template <class T>
pair<T, T> uread() {
char c = getchar();
while (c < '0' || c > '9') {
c = getchar();
}
T num1 = 0, num2 = 0;
while (c >= '0' && c <= '9') {
num1 = ((num1 << 1) + (num1 << 3) + (c ^ '0')) % p1;
num2 = ((num2 << 1) + (num2 << 3) + (c ^ '0')) % p2;//边读入边取模
c = getchar();
}
return {num1, num2};
}
pair<ull, ull> a[N];
int main(int argc, const char * argv[]) {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
a[i] = uread<ull>();
}
vector<pair<ull, ull>> vec;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
vec.push_back({(a[i].first * (unsigned __int128)a[j].first) % p1, (a[i].second * (unsigned __int128)a[j].second) % p2});//也可以使用快速乘
}
}
sort(vec.begin(), vec.end());
ull ans = 0;
for (int i = 1; i <= n; ++i) {
ans += upper_bound(vec.begin(), vec.end(), a[i]) - lower_bound(vec.begin(), vec.end(), a[i]);//等于 a[i] 的有多少个
}
printf("%llu\n", ans);
return 0;
}
G
如果把题目弱化,每次询问整个序列 \(\leq x\) 的数的和,怎么做?
这不是普及组选手必须要会的内容吗?把整个数组排序,每次二分找到最后一个小于等于 \(x\) 的位置 \(i\),答案就是前 \(i\) 个数的和,先预处理一遍前缀和就行。
很可惜,这道题有区间,不能对整个数组排序。那怎么想办法把上述思想推广到这道题呢?我们想一下,如果一个子区间(下面称为块)被 \(l,r\) 完全包括,那么对它排序肯定是不影响的。所以我们可以在整个序列上划分成若干个大小相同的块,每个块先预处理:把区间内每个数排序后做前缀和,在询问的时候如果整块都被区间包括了,那么可以直接在整块上按照弱化版的思路获取答案。那如果一些数所在的块没有被完整包含,那么就直接暴力统计答案。
那如何调整每一块的长度可以获得最优的复杂度呢?我们把每一块的块长设为 \(b\) 再进行分析。
首先来看预处理的复杂度。我们将整个序列分成了 \(\frac{n}{b}\) 块,每一块进行了一次复制原数组,一次排序,一次统计前缀和。所以时间复杂度是 \(O(\frac{n}{b} \times b + (\frac{n}{b} \times \log \frac{n}{b}) \times b + \frac{n}{b} \times b)\) = \(O(n \log n)\),这里与 \(b\) 无关。
接下来来看单次询问的复杂度。每次询问时最坏情况会暴力统计 \(2b\) 个数,也就是把左端点正好是一块的起点,右端点正好是一块的终点。块最坏情况下有 \(\frac{n}{b}\) 个,每块二分一下,复杂度为每次 \(\log \frac{n}{b}\)。所以单次询问复杂度为 \(O(b + \frac{n}{b} \times \log \frac{n}{b})\)。
我们发现只有询问的复杂度和 \(b\) 相关,所以我们来分析这一块。我们再看一遍式子:\(O(b + \frac{n}{b} \times \log \frac{n}{b})\)。注意到随着 \(b\) 的减小,\(\frac{n}{b} \times \log \frac{n}{b}\) 是越大的。如果我们保证了一个小另一个大,或者一个大另一个小,复杂度上都是不优的。因为出题人可以出比较大的极端情况。所以最优时,我们应该将两个取等。所以解出 \(b = \frac{n}{b} \times \log \frac{n}{b}\),即可得出最优块长。为了简便我们把 \(\log \frac{n}{b}\) 看成 \(\log n\),原式变化为 \(b = \frac{n}{b} \times \log n\),两边同时乘 \(b\),得 \(b^2 = n \log n\),所以最优块长 \(b\) 定为 \(\sqrt{n \log n}\) 时间复杂度最优,为 \(O(n \log n + q \sqrt{n \log n})\)。空间复杂度为 \(O(n)\)。这种把序列分成若干块,每一块预处理,询问时对于整块用预处理的结果查询,散块暴力,平衡预处理、散块暴力、整块查询的思想就叫做分块。
实现
#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;
typedef long long ll;
template <class T>
T uread() {
char c = getchar();
while (c < '0' || c > '9') {
c = getchar();
}
T num = 0;
while (c >= '0' && c <= '9') {
num = (num << 1) + (num << 3) + (c ^ 48);
c = getchar();
}
return num;
}
const int N = 2e5 + 1;
const int B = 1877 + 1;
int a[N], bel[N];//第 i 个数属于的块的编号
vector<int> bNum[107 + 1];//每个块排好序的数字们
ll preSum[107 + 1][B];//第 i 个块前 j 个数的和
int main(int argc, const char * argv[]) {
int n = uread<int>();
int blockSize = ceil(sqrt(n * log2(n)));//最大为 1877
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
bNum[bel[i] = (i + blockSize - 1) / blockSize].push_back(a[i]);
}
for (int i = 1; i <= bel[n]/*最大为 107*/; ++i) {
sort(bNum[i].begin(), bNum[i].end());
int len = (int)bNum[i].size();
for (int j = 0; j < len; ++j) {
preSum[i][j] = preSum[i][j - 1] + bNum[i][j];
}
}
int m = uread<int>();
ll last = 0;
for (int _ = 1; _ <= m; ++_) {
ll A = uread<ll>(), B = uread<ll>(), Y = uread<ll>();
int l = (int)(A ^ last), r = (int)(B ^ last), x = (int)(Y ^ last);
if (bel[l] == bel[r]) {//同一块直接暴力
ll ans = 0;
for (int i = l; i <= r; ++i) {
if (a[i] <= x) {
ans += a[i];
}
}
printf("%lld\n", (last = ans));
continue;
}
ll ans = 0;
for (int i = l; bel[i] == bel[l]; ++i) {
if (a[i] <= x) {
ans += a[i];
}
}//解决左端点散块
for (int i = bel[l] + 1; i < bel[r]; ++i) {
int pos = (int)(upper_bound(bNum[i].begin(), bNum[i].end(), x) - bNum[i].begin());
if (pos != 0) {//注意别写越界
ans += preSum[i][pos - 1];
}
}//解决整块
for (int i = r; bel[i] == bel[r]; --i) {
if (a[i] <= x) {
ans += a[i];
}
}//解决右端点散块
printf("%lld\n", (last = ans));
}
return 0;
}
总结
这场比赛总体来说还是比较 edu 的。打好 abc 的技巧就在于多看题,多做题,多总结,多思考为什么要这样做。这样以后看到相似题才能很快地转换过去。
abc339 详解的更多相关文章
- Linq之旅:Linq入门详解(Linq to Objects)
示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...
- 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)
一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...
- EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解
前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...
- Java 字符串格式化详解
Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...
- Android Notification 详解(一)——基本操作
Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...
- Android Notification 详解——基本操作
Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...
- Git初探--笔记整理和Git命令详解
几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...
- Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)
Android XML shape 标签使用详解 一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...
- Node.js npm 详解
一.npm简介 安装npm请阅读我之前的文章Hello Node中npm安装那一部分,不过只介绍了linux平台,如果是其它平台,有前辈写了更加详细的介绍. npm的全称:Node Package M ...
- .NET应用和AEAI CAS集成详解
1 概述 数通畅联某综合SOA集成项目的统一身份认证工作,需要第三方系统配合进行单点登录的配置改造,在项目中有需要进行单点登录配置的.NET应用系统,本文专门记录.NET应用和AEAI CAS的集成过 ...
随机推荐
- myeclipse过期问题
安装myeclipse后就会遇到过期问题.如下代码永久解决过期问题. import java.io.BufferedReader; import java.io.IOException; import ...
- flexible+rem移动端适配
- P1228-递归【黄】
这道大递归我一开始就找对了方向,不过了MLE,然后从网上搜索到了一个贼有用的概念--尾递归,即如果递归的下一句就是return且没有返回值或者返回值不含有递归函数则编译器会做优化,不会压入新的函数而是 ...
- idea中配置mybatis 映射文件模版及 mybatis plus 自定义sql
本文为博主原创,未经允许不得转载: mybatis plus 使用过程中已经很大程度提升了我们开发的效率,因为它内部已经对单表的操作进行了完美的封装,但是关联表操作时, 这时就需要自己定义sql,自定 ...
- 云计算&虚拟化 技术名词汇总
云计算&虚拟化 技术名词汇总 目录 云计算&虚拟化 技术名词汇总 虚拟化方向 QEMU/qemu VMM virtual machine monitor (虚拟机监管器) Hyperv ...
- 2023第十四届极客大挑战 — CRYPTO(WP全)
浅谈: 本次大挑战我们队伍也是取得了第一名的成绩,首先要感谢同伴的陪伴和帮助.在共同的努力下终不负期望! 但遗憾的是我们没有在某个方向全通关的,呜呜呜~ 继续努力吧!要学的还很多.明年有机会再战!!加 ...
- css - 伪元素清除浮动
.clearfix:after{ content:""; /*设置内容为空*/ height:0; /*高度为0*/ line-height:0; /*行高为0*/ display ...
- .net core 3.0 获取 IServiceProvider 实例
.net core 3.0后,获取IServiceProvider需要绕点弯路 首先,新建一个类: public class MyServiceProviderFactory : IServicePr ...
- Docker-02应用部署案例
1.Docker部署mysql 拉取mysql镜像 # 查询mysql镜像 docker search mysql # 拉取镜像命令 docker pull centos/mysql-57-cento ...
- window-子系统-ubuntu
window-子系统-ubuntu 1. 背景 提供类Linux开发环境(命令行.文件系统.进程管理.网路) 2. 安装 A. wsl 安装 下载链接: https://wslstorestorage ...