首页 > 其他分享 >abc339 详解

abc339 详解

时间:2024-02-05 15:33:25浏览次数:31  
标签:abc339 const log int times 详解 frac include

第一篇整场题解纪念我第一次 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 的技巧就在于多看题,多做题,多总结,多思考为什么要这样做。这样以后看到相似题才能很快地转换过去。我这次能 AK abc 的原因可能也有运气——每一道题我都见过的相似的题并弄懂思考明白了。

标签:abc339,const,log,int,times,详解,frac,include
From: https://www.cnblogs.com/SkyWave20100601/p/18007307

相关文章

  • 淘宝/天猫商品详情API:返回值参数详解及商业逻辑实现
    在电子商务的高速发展过程中,API接口扮演了至关重要的角色。对于淘宝和天猫这样的大型电商平台,商品详情API是商家与消费者信息沟通的桥梁。本文将深入探讨这一API的返回值参数,并展示如何通过编程利用这些数据实现商业逻辑。一、商品详情API的核心作用商品详情页是电商体验中的重要环......
  • RocketMQ_详细配置与使用详解
    为什么要用MQ 应用解耦系统的耦合性越高,容错性就越低。以电商应用为例,用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障或者因为升级等原因暂时不可用,都会造成下单操作异常,影响用户使用体验。 使用消息队列解耦合,系统的耦合性就会提高了。......
  • 【Java基础】Java线程的六种状态详解
    NEW状态当创建一个Thread对象但尚未调用其start()方法时,线程处于NEW状态。在这个状态下,线程并未启动,仅完成了初始化阶段。RUNNABLE状态RUNNABLE是Java中较为特殊的一个状态,它涵盖了传统操作系统中的就绪和运行两种状态。当线程已启动且CPU调度器为其分配了时间片或线程正在等待系......
  • 【adb基础】adb详解及使用
    dumpsysdumpsys是Android系统的调试工具,提供有关系统服务的信息pm(PackageManager)主要用于获取和安装在Android设备上的应用信息。ADB运行架构adbclient--->adbshellechoxxxadbserver--->adb-ltcp:5037fork-serverserver--reply-fd4(可使用命令查看此服务ps......
  • Java 运算符详解与字符串处理技巧
    Java运算符算术运算符算术运算符用于执行常见的数学运算。运算符名称描述示例+加法将两个值相加x+y-减法从一个值中减去另一个值x-y*乘法将两个值相乘x*y/除法将一个值除以另一个值x/y%取模返回除法余数x%y++自增将变量......
  • mysql数据库--行级锁,间隙锁和临键锁详解
    转载链接地址:MySQL数据库——锁-行级锁(行锁、间隙锁和临键锁)介绍行级锁,每次操作锁住对应的行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB存储引擎中。InnoDB的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。对于行级......
  • 【算法专题】排序详解
    各种排序快速排序、归并排序、桶排序、堆排序1、快速排序(quick_sort)时间复杂度:\(O(nlogn)\)//快速排序模版voidquick_sort(intq[],intl,intr)//数组,左端点,右端点{if(l>=r)return;//“>>”:右移运算符,相当于除以2intx=q[l+r>>1],......
  • 第一章学习Markdown语法详解
    Markdown学习一、标题:一级标题一个井号空格加标题名字就可以了二级标题两个井号空格加标题名字就可以了三级标题三个井号空格加标题名字就可以了四级标题四个井号空格加标题名字就可以了五级六级标题把对应的#写够即可。注意最多只支持到六级标题二、字体Hello,World!......
  • 全网最牛逼的OSPF LSA类型详解
    OSPF定义了不同类型的LSA,每种类型承载着不同的网络拓扑信息。帮助路由器建立完整的拓扑视图,从而实现高效的路由计算和数据传输。今天就给你来一篇超实用的讲解干货,运用实际案例给你说明白!在正式讲解之前,先说明一下今天使用的讲解环境哈:·R1、R2、R3、R4四台路由器运行OSPF。·设......
  • 详解磁盘IO、网络IO、零拷贝IO、BIO、NIO、AIO、IO多路复用(select、poll、epoll)
    1.什么是I/O在计算机操作系统中,所谓的I/O就是输入(Input)和输出(Output),也可以理解为读(Read)和写(Write),针对不同的对象,I/O模式可以划分为磁盘IO模型和网络IO模型。IO操作会涉及到用户空间和内核空间的转换,先来理解以下规则:   内存空间分为用户空间和内核空间,也称为用户缓冲区和内......