SuntoryProgrammingContest2024(AtCoder Beginner Contest 357)
A - Sanitize Hands
题意:给定一个序列和m,问m按顺序减去这个序列,m >= 0情况下最多能减多少个数
思路:前缀和 + prev(upper_bound())
总结:disinfectan(消毒ji), disinfect(消毒,杀毒), aliens(外星人),
void solve() {
int n, m;
cin >> n >> m;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
if (i) {
a[i] += a[i - 1];
}
}
cout << (upper_bound(a.begin(), a.end(), m) - a.begin()) << '\n';
}
B - Uppercase and Lowercase
题意:给个奇数长度字符串,如果大写字符数量大于小写字符数量,全小写,否则全大写。
思路:记录数量直接变。
总结:没读透彻题目,还在思考如果两种字符出现次数相等怎么办。才发现是奇数长度的字符串。
void solve() {
string s;
cin >> s;
int cnt0 = 0;
int cnt1 = 0;
for (const auto& x : s) {
if ('a' <= x && x <= 'z') {
cnt0++;
}
else {
cnt1++;
}
}
for (auto& x : s) {
if (cnt0 < cnt1) {
if ('a' <= x && x <= 'z') {
x += 'A' - 'a';
}
}
else if ('A' <= x && x <= 'Z'){
x += 'a' - 'A';
}
}
cout << s << '\n';
}
C - Sierpinski carpet
题意:给定一个3的k次幂的矩阵,每个矩阵可以分解为9个3的k-1次幂的小矩阵,其中中间的矩阵需要是白色,其他矩阵作为3的k-1次矩阵存在。输出该矩阵。
思路:深度遍历,每次给定当前矩阵的左上和右下坐标,在矩阵中遍历,中间矩阵染白,其他矩阵递归处理。
总结:在坐标的处理上把握不准确:一共有3行3列,我们只要考虑当前行列左上角的坐标即可,右下角的坐标直接用左上角的坐标加长度就行。左上角的坐标对于行来说,第2行加一个单位长度,第3行加两个单位长度,列也同理。然后其实也可以不用右下角坐标参数,只要传一个边长进去就行,这个边长一定是3的整数次幂。
在输出矩阵的时候wa了两次,习惯了数组输出中间加' '...
carpet(地毯)
void solve() {
int n;
cin >> n;
int m = pow(3, n);
vector<vector<char>> mat(m + 1, vector<char>(m + 1, '#'));
function<void(int, int, int, int)> dfs = [&](int x1, int y1, int x2, int y2) {
if (x1 == x2 && y1 == y2) {
return;
}
int d = (x2 - x1 + 1) / 3;
for (int i = 1; i <= 3; ++i) {
for (int j = 1; j <= 3; ++j) {
int u1 = x1 + (i - 1) * d;
int v1 = y1 + (j - 1) * d;
int u2 = u1 + d - 1;
int v2 = v1 + d - 1;
if (i == 2 && j == 2) {
while (u1 <= u2) {
for (int k = v1; k <= v2; ++k) {
mat[u1][k] = '.';
}
u1++;
}
}
else if (d > 1){
dfs(u1, v1, u2, v2);
}
}
}
};
dfs(1, 1, m, m);
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= m; ++j) {
cout << mat[i][j];
}
cout << '\n';
}
}
D - 88888888
题意:给定一个数字x(x <= 1e18),问这个数字重复x次对998244353取模是多少。
思路:从最高的前x位考虑,每次取模后的余数m+后x位是下一次要参与到取模计算的位,设t为pow(10, x的长度) % mod,
可以得出公式:(((x % mod * t) + x) % mod * t + x) + x) * t % mod + ...
x % mod是一个定值,设为res,先不考虑mod,公式写为(((res * t) + x) * t + x) * t + ...
第一项:res = x % mod
第二项:res * t + x
第三项:(res * t + x) * t + x = res * t² + x * t + x
第四项: res * pow(t, 3) + x * pow(t, 2) + x * t + x
..
第n项: res * pow(t, n - 1) + x * (pow(t, 1) + pow(t, 2) + .. + pow(t, n - 2))
根据上述公式,第一项快速幂直接得出结果,第二项是一个等比数列,根据等比数列公式 s = t * (1 - pow(t, n - 2)) / 1 - t。由于t是一个模运算的值,所以这里除法要用逆元的形式来求。
基于上述推导,可以使用快速幂+乘法逆元直接计算了,时间复杂度很低,乘法逆元使用费马小定理即可。
总结:赛时公式推导出来了,但是总感觉数字溢出了,最后比赛结束了才发现是除法没有去逆元。 这题目很棒。
快速幂的时候要注意次数一定要>=0,如果输入为1时,不需要按推导公式计算。
constexpr int mod = 998244353;
void solve() {
long long n;
cin >> n;
long long t = 1;
if (n == 1) {
cout << 1 << endl;
return;
}
for (auto x = n; x; x /= 10, t = (t * 10) % mod);
long long res = n % mod;
res = res * (1 + fastPower(t, n - 1, mod)) % mod +
n % mod * (t * (1 - fastPower(t, n - 2, mod)) % mod * fermatInverse(1 - t, mod) % mod) % mod;
cout << res % mod << endl;
}
E - Reachability in Functional Graph
题意:给定n个点的图,保证每个点的出度为1。sigma(求每个点能到达的其他点的数量)
思路:先用dsu求出所有的环,环中每个点的可达点数量就是环的大小。
然后遍历所有入度为0的点,求出环到点的距离,加上环的大小就是该点可达的点数,最后对每个点求和就行。
总结:看了之后没思路,不知道dsu跟dfs怎么用,其实就是环内的点的数量都能确定,然后环外的点入度一定是0,再依次判断一下即可。dsu类中的友元函数的缺省值要定义在外面,不然C++20会报错。
/*
* DisjointSet(并查集)
* 设计思想:保留了基本的合并查询功能,采用了宏定义的方式,可手动指定增加新功能。
* 基于面向对象的编程思想,本方法尽可能多的隐藏了内部实现的细节,并且将必要的编程接口暴露在外部,并需要对这些接口进行直接的修改。
* 在该设计中,需要修改的接口有:
* USE_DSU_SET_ELEMENT:是否获取集合中的元素。
* 如果要保留并查集中每个集合中的具体元素,将该定义设置为true。
* USE_DSU_WEIGHT:是否使用带权并查集。
* 如果使用带权并查集,将该定义设置为true。
* 并且实现两个友元函数mergeWeights和compressWeights。
* unionWeights:在两个集合合并时调用,需要手动实现初始化权重的细节。
* compressWeights():在路径压缩时调用,需要手动实现权重更新的细节。
* 并查集的主体所有必备的方法都已实现,无需修改。
*
* gitHub(仓库地址): https://github.com/100000000000000000000000000000000/Programming-template-for-OJ
*/
#define USE_DSU_SET_ELEMENT 0
#define USE_DSU_WEIGHT 1
class DisjointSet {
friend void unionWeights(DisjointSet& dsu, int x, int y, int px, int py, long long value);
friend void compressWeights(DisjointSet& dsu, int x, int y);
public:
DisjointSet(int sz) :
sz_(sz),
num_sets_(sz)
{
fa_.resize(sz_);
std::iota(fa_.begin(), fa_.end(), 0);
set_size_.assign(sz_, 1);
#if USE_DSU_WEIGHT
weight_.resize(sz_);
#endif
#if USE_DSU_SET_ELEMENT
elements_.resize(sz_);
for (int i = 0; i < sz_; ++i) {
elements_[i].emplace_back(i);
}
#endif
}
inline int findSet(int x) {
if (fa_[x] == x) {
return x;
}
int par = fa_[x];
fa_[x] = findSet(fa_[x]);
#if USE_DSU_WEIGHT
compressWeights(*this, x, par);
#endif
return fa_[x];
}
inline int getSetSize(int x) {
return set_size_[findSet(x)];
}
#if USE_DSU_WEIGHT
long long getWeight(int x) {
findSet(x);
return weight_[x];
}
#endif
inline int countSets() {
return num_sets_;
}
inline bool isSameSet(int x, int y) {
return findSet(x) == findSet(y);
}
bool unionSet(int x, int y, long long value = 0) {
int px = findSet(x);
int py = findSet(y);
if (px == py) {
return false;
}
fa_[px] = py;
num_sets_--;
#if USE_DSU_WEIGHT
unionWeights(*this, x, y, px, py, value);
#endif
set_size_[py] += set_size_[px];
#if USE_DSU_SET_ELEMENT
elements_[y].insert(elements_[y].end(), elements_[x].begin(), elements_[x].end());
elements_[x].clear();
#endif
return true;
}
#if USE_DSU_SET_ELEMENT
inline std::vector<int> getSetElements(int x) {
return elements_[findSet(x)];
}
#endif
private:
int sz_;
int num_sets_;
std::vector<int> fa_;
std::vector<int> set_size_;
std::vector<long long> weight_;
};
/*
这里x和y是操作时的集合节点,要把x所在集合合并到y。px和py是前两者的集合代表元素。
value是一个缺省值,代表指定x->y的权值,默认为0。
*/
void unionWeights(DisjointSet& dsu, int x, int y, int px, int py, long long value = 0) {
}
/*
这里是路径压缩时的更新权重操作,y是x压缩前的直接父亲节点。
*/
void compressWeights(DisjointSet& dsu, int x, int y) {
}
void solve() {
int n;
cin >> n;
vector<int> a(n + 1);
vector<int> indegree(n + 1);
for (int i = 1; i <= n; ++i) {
cin >> a[i];
indegree[a[i]]++;
}
DisjointSet dsu(n + 1);
vector<int> sz(n + 1);
for (int i = 1; i <= n; ++i) {
if (!dsu.isSameSet(i, a[i])){
dsu.unionSet(i, a[i]);
}
else {
vector<int> c;
for (int u = i; ; u = a[u]) {
c.push_back(u);
if (a[u] == i) {
break;
}
}
for (const auto& u : c) {
sz[u] = (int)c.size();
}
}
}
function<int(int)> dfs = [&](int u) {
if (sz[u]) {
return sz[u];
}
int res = dfs(a[u]);
return sz[u] = res + 1;
};
for (int i = 1; i <= n; ++i) {
if (sz[i] == 0 && indegree[i] == 0) {
dfs(i);
}
}
long long ans = 0;
for (int i = 1; i <= n; ++i) {
ans += sz[i];
}
cout << ans << endl;
}
F - Two Sequence Queries
题意:给定2个长度为n的数组a和b,m个操作。对于m个操作,对于区间l和r,每次操作有3种,将a中该区间每个元素+x,或者b中区间每个元素+x,或者求区间中sigma(a[i] * b[i])(l <= i <= r)
思路:长度固定,直接套静态线段树模板。因为还要取模,所以线段树中维护的元素数据类型使用MInt。每个树节点维护:数组a的和,数组b的和,乘积的和。 对于节点合并的操作,直接元素相加即可。对于节点更新的操作,需要考虑三种情况:增加了a,增加了b,a和b的和都增加了,把这三种情况的变动更新到区间乘积和上,再更新a和b的区间和即可。 对于懒人标记的向下更新,直接求和叠加即可,增加数值的操作跟顺序无关。
总结:一开始在合并区间时忘记更新区间和的值了。然后在更新区间值时,没有正确的考虑a和b都增加了数值的情况。都增加数值时,sum有一部分是add_a*sum_b的贡献,还有一部分是add_b * sum_a,还有一部分是add_a * add_b * segnemnt_length。
/*
* StaticSegmentTree(静态线段树)
* 设计思想:静态线段树主要用于先给出一组输入(一般这种输入确定了树中每个节点都会被访问到,所以直接静态开点),并且给出若干个修改和查询的情况。
* 基于面向对象的编程思想,本方法尽可能多的隐藏了内部实现的细节,并且将必要的编程接口暴露在外部,并需要对这些接口进行直接的修改。
*
* 在该设计中,需要修改的接口有:
* UpdateNode:该结构体存储了区间上的操作类型:需要自定义结构体变量成员及构造函数;
* 懒人标记的向下传递方法:mergeLazyMarks();
* 懒人标记被更新后的方法:clear()。
* StaticSegmentTreeNode: 需要在该结构体中写出要维护的区间参数,初始化函数;
* 该类重载了+来实现左右孩子区间合并:在重载函数中实现细节;
* 实现区间上更新懒人标记值的方法:applyUpdate();
* 建树必须指定模板参数True或False,代表是否使用懒人标记。
* 静态线段树的主体所有必备的方法都已实现,无需修改,有的题目有剪枝操作请自行添加实现。
*
* gitHub(仓库地址): https://github.com/yxc-s/programming-template.git
*/
/*
该节点定义在区间上的操作,可以根据输入的类型重写该结构体。
*/
struct UpdateNode {
/* 自定义区间要执行的操作变量。*/
MInt add_a = 0;
MInt add_b = 0;
/* 自定义初始化构造函数。*/
UpdateNode(){}
UpdateNode(MInt a, MInt b): add_a(a), add_b(b){}
/* 懒人标记向下传递时调用,涉及区间操作必须实现。*/
inline void mergeLazyMarks(const UpdateNode& parent_node, int segment_length) {
add_a += parent_node.add_a;
add_b += parent_node.add_b;
}
/* 清除懒人标记,涉及区间操作必须实现。 */
inline void clear() {
add_a = add_b = 0;
}
};
struct StaticSegmentTreeNode {
/* 在区间上要维护的变量值,必须实现。*/
MInt sum_a = 0, sum_b = 0, sum = 0;
/* 构造函数,必须实现。*/
explicit StaticSegmentTreeNode(MInt value = 0): sum(value) {}
/* 重载左右孩子区间合并操作,必须实现。*/
friend StaticSegmentTreeNode operator + (const StaticSegmentTreeNode& a, const StaticSegmentTreeNode& b) {
StaticSegmentTreeNode res{a.sum + b.sum};
res.sum_a = a.sum_a + b.sum_a;
res.sum_b = a.sum_b + b.sum_b;
return res;
}
/* 区间更新方法,必须实现。*/
inline void applyUpdate(const UpdateNode& value, int segment_length) {
sum += sum_b * value.add_a;
sum += sum_a * value.add_b;
sum += value.add_a * value.add_b * segment_length;
sum_a += value.add_a * segment_length;
sum_b += value.add_b * segment_length;
}
};
/*
静态线段树,query返回类型必须是NODE_TYPE类型,更新数值类型必须是UpdateNode。
初始化模板必须指定true或者false。
该树总是约定区间左端点从1开始(避免(0 << 1)的情况)。
可通过树节点类型的数组初始化建树(推荐,建树时间复杂度更低)。
可通过要维护的区间的最大右端点下标来建树。
*/
template<const bool USE_LAZY_FLAG>
class StaticSegmentTree {
using LAZY_TYPE = UpdateNode;
using NODE_TYPE = StaticSegmentTreeNode;
public:
constexpr explicit StaticSegmentTree(unsigned int n) : n_(n) {
st_.resize(4 * n_);
if constexpr (USE_LAZY_FLAG){
lazy_.resize(4 * n_);
has_lazy_.resize(4 * n_);
}
}
StaticSegmentTree(const std::vector<NODE_TYPE>& s) : n_(static_cast<int> (s.size()) - 1) {
st_.resize(4 * n_);
if constexpr (USE_LAZY_FLAG){
lazy_.resize(4 * n_);
has_lazy_.resize(4 * n_);
}
build(s, 1, 1, n_);
}
/* 单点更新。*/
inline void update(int i, const LAZY_TYPE& value) {
update(1, 1, n_, i, i, value);
}
/* 更新区间值。*/
inline void update(int i, int j, const LAZY_TYPE& value) {
update(1, 1, n_, i, j, value);
}
/* 获取区间节点。*/
inline NODE_TYPE query(int i, int j) {
return query(1, 1, n_, i, j);
}
private:
unsigned int n_;
std::vector<NODE_TYPE> st_;
std::vector<LAZY_TYPE> lazy_;
std::vector<bool> has_lazy_;
/* 区间更新。*/
void update(int p, int l, int r, int i, int j, const LAZY_TYPE& value) {
if constexpr (USE_LAZY_FLAG) {
propagate(p, l, r);
}
if (i > j) {
return;
}
if (l >= i && r <= j){
if (USE_LAZY_FLAG == true){
lazy_[p] = value;
has_lazy_[p] = true;
propagate(p, l, r);
return;
}
else if (l == r){
st_[p].applyUpdate(value, 1);
return;
}
}
int mid = (l + r) >> 1;
update(p << 1, l, mid, i, std::min(mid, j), value);
update(p << 1 | 1, mid + 1, r, std::max(mid + 1, i), j, value);
st_[p] = st_[p << 1] + st_[p << 1 | 1];
};
/* 区间查询。*/
NODE_TYPE query(int p, int l, int r, int i, int j) {
if constexpr (USE_LAZY_FLAG) {
propagate(p, l, r);
}
if (l >= i && r <= j) {
return st_[p];
}
int mid = (l + r) >> 1;
if (j <= mid) {
return query(p << 1, l, mid, i, j);
}
else if (i > mid) {
return query(p << 1 | 1, mid + 1, r, i, j);
}
else {
return (query(p << 1, l, mid, i, mid) + query(p << 1 | 1, mid + 1, r, mid + 1, j));
}
}
/* 初始化构造。*/
void build(const std::vector<NODE_TYPE>& s, int p, int l, int r) {
if (l == r) { st_[p] = s[l]; }
else {
int mid = (l + r) >> 1;
build(s, p << 1, l, mid);
build(s, p << 1 | 1, mid + 1, r);
st_[p] = st_[p << 1] + st_[p << 1 | 1];
}
}
/* 懒人标记向下传播。*/
inline void propagate(int p, int l, int r) {
if (has_lazy_[p] == true) {
st_[p].applyUpdate(lazy_[p], r - l + 1);
if (l != r) {
lazy_[p << 1].mergeLazyMarks(lazy_[p], r - l + 1);
lazy_[p << 1 | 1].mergeLazyMarks(lazy_[p], r - l + 1);
has_lazy_[p << 1] = has_lazy_[p << 1 | 1] = true;
}
has_lazy_[p] = false;
lazy_[p].clear();
}
}
};
using StaticSegTree = StaticSegmentTree<true>;
using StaticSegNode = StaticSegmentTreeNode;
/*
ToDoList:
*/
using namespace std;
void preProcess() {
}
void solve(){
int n, m;
cin >> n >> m;
vector<StaticSegNode> a(n + 1);
for (int i = 1; i <= n; ++i){
cin >> a[i].sum_a;
}
for (int i = 1; i <= n; ++i){
cin >> a[i].sum_b;
a[i].sum = a[i].sum_a * a[i].sum_b;
}
StaticSegTree st(a);
while (m --){
int t, l, r;
cin >> t >> l >> r;
if (t == 1){
int x;
cin >> x;
st.update(l, r, UpdateNode{x, 0});
}
else if (t == 2){
int x;
cin >> x;
st.update(l, r, UpdateNode{0, x});
}
else{
cout << st.query(l, r).sum << '\n';
}
}
}
有模板写题就是好用,只要维护固定的几个函数即可。
模板放在了下面的仓库,如果使用,请点进去点个star。
https://github.com/yxc-s/programming-template/tree/master
该仓库是一个新仓库,旨在打造一个通用的C++算法编程竞赛模板,包含数据结构,数论等各种实用的算法编程模板。如果您使用的语言不是C++,也可以将对应的代码实现翻译成其他语言来使用。