2023-11-11 22:29阅读: 682评论: 0推荐: 0

AtCoder Beginner Contest 328

A - Not Too Hard (abc328 A)

题目大意

给定n个数字和一个数 x

问不大于 x的数的和。

解题思路

按找要求累计符合条件的数的和即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, x;
cin >> n >> x;
int sum = 0;
for (int i = 0; i < n; ++i) {
int a;
cin >> a;
sum += a * (a <= x);
}
cout << sum << '\n';
return 0;
}


B - 11/11 (abc328 B)

题目大意

给定一年的月数和一个月的天数。

问有多少对(i,j),表示第 i个月的第 j日, i,j的数位上每个数字都是一样的。

解题思路

范围只有O(1002),枚举所有的 (i,j)逐个判断即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
int ans = 0;
auto ok = [&](int x, int y) {
int t = x % 10;
while (x) {
if (t != x % 10)
return false;
x /= 10;
}
while (y) {
if (t != y % 10)
return false;
y /= 10;
}
return true;
};
for (int i = 1; i <= n; ++i) {
int x;
cin >> x;
for (int j = 1; j <= x; ++j) {
ans += ok(i, j);
}
}
cout << ans << '\n';
return 0;
}


C - Consecutive (abc328 C)

题目大意

给定一个字符串s和若干个询问。

每个询问问 s[l..r]子串中,有多少对相邻相同字母的下标。

解题思路

a[i]=1表示s[i]==s[i+1]a[i]=0表示 s[i]s[i+1]

每个询问就是问 i=lr1a[i]

预处理数组a前缀和 即可O(1)回答询问。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
string s;
cin >> n >> q >> s;
vector<int> sum(n);
for (int i = 0; i < n - 1; ++i) {
sum[i] = (s[i] == s[i + 1]);
if (i)
sum[i] += sum[i - 1];
}
while (q--) {
int l, r;
cin >> l >> r;
--l, --r;
int ans = 0;
if (r)
ans += sum[r - 1];
if (l)
ans -= sum[l - 1];
cout << ans << '\n';
}
return 0;
}


D - Take ABC (abc328 D)

题目大意

给定一个仅包含ABC的字符串s,每次将最左边的ABC删除,直至不能删。

问最后的字符串。

解题思路

可以从左到右依次将s的每个字符加入栈中,一旦栈顶构成ABC就弹栈。

模拟即可。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
string s;
cin >> s;
string st;
for (auto& i : s) {
st += i;
if (st.size() >= 3) {
int n = st.size();
if (st.substr(st.size() - 3) == "ABC") {
st.pop_back();
st.pop_back();
st.pop_back();
}
}
}
cout << st << '\n';
return 0;
}


E - Modulo MST (abc328 E)

题目大意

给定一张图,问模k意义下的最小生成树的代价。

解题思路

注意是模k意义下的最小代价,在求生成树过程中的每一个值都有可能在加入某条边后超过k而变的最小,成为最后的答案。

注意点数只有8,边数最多也只有 28,因此总的方案数只有 228=2e8。暴力可行。即可以保留中间的所有结果。

考虑prim求生成树做法,从1号点不断往外拓展,保留当前最小的结果。我们借用这个想法,但保留当前所有的结果:设 dp[i]表示所有点与1号点连通性为 i的情况下,所有生成树的结果(i&1=0的所有情况都是不合法的,1号点肯定与 1号点连通)。很显然dp[1]=[0]

然后枚举下一条边连接,更新所有结果即可。由于可能会有重复的值(同一个生成树可以从不同的加边顺序得到),所以用 set

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
LL k;
cin >> n >> m >> k;
vector<vector<pair<int, LL>>> edge(n);
for (int i = 0; i < m; ++i) {
int u, v;
LL w;
cin >> u >> v >> w;
--u, --v;
edge[u].push_back({v, w});
edge[v].push_back({u, w});
}
int up = (1 << n);
int st = n - 1;
vector<set<LL>> candi(up);
candi[1].insert(0);
for (int i = 0; i < up; ++i) {
if (~i & 1)
continue;
for (int u = 0; u < n; ++u) {
if ((~i >> u) & 1)
continue;
for (auto& [v, w] : edge[u]) {
if ((i >> v) & 1)
continue;
for (auto& val : candi[i])
candi[i | (1 << v)].insert((val + w) % k);
}
}
}
cout << *ranges::min_element(candi.back()) << '\n';
return 0;
}


F - Good Set Query (abc328 F)

题目大意

给定数字n,依次给定q个条件 (ai,bi,di)

对于一个条件集合s,如果存在一个长度为n数组 x,对于这个集合里的所有条件,都满足xaixbi=di,那么这个集合是好的。

初始集合为空,依次对每个条件,如果加入到集合后,集合是好的,则加入到集合中。

问最后集合的元素。

解题思路

条件相当于是规定了数组x元素之间的差的关系。

对于一个条件(a,b,d),我们可以连一条 ab的边,边权为 d,反向边的边权为 d

考虑一个集合不是好时,此时形成的图是怎样的。

当加入一个条件(a,b,d),集合可能还是好的,但也可能变得不好,

如果还是好的,有两种情况:

  • 一是a,b原先没有什么联系,即不连通,加了条边之后连通了,仅此而已。
  • 二是a,b是连通的,加了 ab这条边后会形成一个环,环的边权和为 0,或者说 ab存在两条路径,其边权和相等。

在第二种情况下,这个条件就是多余的,我们可以不管这个条件,即不加这条边。此时图就没有环,即是一棵树(或森林)。

树是一个非常好的图,有着树上路径唯一的性质,因此情况二下, 我们可以很容易求出 ab的路径和,然后与 d比较,相等则说明加入这个条件后,集合还是好的。

而如果不相等,则说明不能加入这个条件,即有条件冲突了,说明 ab存在两条边权和不一样的路径

所以问题就剩下,如何在动态加边的情况下,求出ab的长度。

如果是一棵静止的树,一个常用的方法就是预处理每个点到根节点的距离dis[i],那么 ab距离就是 dis[a]dis[b],注意到反向边的边权是负值,所以lca到根的距离恰好抵销了。

而当两棵树合并时,有一棵树的 dis就要全部更新,如果随便选一棵树更新的话,总的时间复杂度可能会是O(n2)。为降低时间复杂度,可以采用启发式合并的策略,即节点树少的树合并到节点树多的树上,这样每次只用更新节点数少的树的 dis。更新就是从合并点开始DFS,更新dis数组。

为计算启发式合并的时间复杂度,可以考虑每个节点的dis的更新次数——每更新一次,其节点所在的连通块大小至少翻倍,那么每个节点最多更新 logn次,其所在的连通块就包含了所有的节点,也就不会再更新了,因此启发式合并的复杂度是 O(nlogn)

用并查集维护连通性,然后树合并时采用启发式合并的策略更新dis数组 ,时间复杂度是 O(q+nlogn)

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
class dsu {
public:
vector<int> p;
vector<int> sz;
vector<LL> dis;
vector<vector<array<int, 2>>> edge;
int n;
dsu(int _n) : n(_n) {
p.resize(n);
sz.resize(n);
dis.resize(n);
edge.resize(n);
iota(p.begin(), p.end(), 0);
fill(sz.begin(), sz.end(), 1);
fill(dis.begin(), dis.end(), 0);
}
inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); }
inline void dfs(int u, int fa) {
for (auto& [v, w] : edge[u]) {
if (v == fa)
continue;
dis[v] = dis[u] - w;
dfs(v, u);
}
}
inline bool unite(int x, int y, int w) {
int fx = get(x);
int fy = get(y);
if (fx != fy) {
if (sz[fx] > sz[fy]) {
swap(x, y);
swap(fx, fy);
w = -w;
}
edge[x].push_back({y, w});
edge[y].push_back({x, -w});
dis[x] = dis[y] + w;
dfs(x, y);
p[fx] = fy;
sz[fy] += sz[fx];
return true;
} else {
return dis[x] == dis[y] + w;
}
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
cin >> n >> q;
dsu ji(n);
for (int i = 1; i <= q; ++i) {
int a, b, d;
cin >> a >> b >> d;
--a, --b;
if (ji.unite(a, b, d))
cout << i << ' ';
}
cout << '\n';
return 0;
}


好像是带权并查集裸题怪不得这么多人过得这么快

还是借用上面计算ab的思想,由于反向边边权取反,因此对于任意一条闭合回路,其边权和一定是 0

由此每个点只需记录到根的距离,而不必关心树的形态,分别考虑在路径压缩和合并时距离的更新即可。

压缩的时候,dis[xroot]=dis[xp[x]]+dis[p[x]],这里的 p[x]是压缩前的父亲, 但dis[p[x]]是更新后的值。
合并的时候, dis[x]=xfx,dis[y]=yfy,令 p[fx]=fy,则 dis[fx]=fxxyfy=dis[x]+w+dis[y]。而 dis[x]会在路径压缩的时候更新。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
class dsu {
public:
vector<int> p;
vector<int> sz;
vector<LL> dis;
int n;
dsu(int _n) : n(_n) {
p.resize(n);
sz.resize(n);
dis.resize(n);
iota(p.begin(), p.end(), 0);
fill(sz.begin(), sz.end(), 1);
fill(dis.begin(), dis.end(), 0);
}
inline int get(int x) {
if (x != p[x]) {
int t = p[x];
p[x] = get(p[x]);
dis[x] += dis[t];
}
return p[x];
}
inline bool unite(int x, int y, int w) {
int fx = get(x);
int fy = get(y);
if (fx != fy) {
p[fx] = fy;
dis[fx] = -dis[x] + dis[y] + w;
sz[fy] += sz[fx];
return true;
} else {
return dis[x] == dis[y] + w;
}
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
cin >> n >> q;
dsu ji(n);
for (int i = 1; i <= q; ++i) {
int a, b, d;
cin >> a >> b >> d;
--a, --b;
if (ji.unite(a, b, d))
cout << i << ' ';
}
cout << '\n';
return 0;
}


G - Cut and Reorder (abc328 G)

题目大意

给定两个数组A,B,可以进行以下两种操作任意次:

  • 选择一个数x,将数组 A分成x+1个部分,然后重新排序,组成一个新数组。代价为xC
  • Ai=Ai+k,代价为|k|

问最小的代价,使得A变成 B

解题思路

首先注意到一点,所有的结果都可以转换成一次操作一+若干次操作二。

由此我们只需考虑如何操作一,因为进行完操作一后。操作二的代价是固定的。

考虑朴素做法,即枚举分界点,有O(2n)种情况,然后对每一个部分进行排序,有 O(n!)种。其复杂度过大了,期间存在重复计算的情况,考虑如何压缩,设计合适的dp状态。

考虑重复计算的状态:设想对数组 A切分成出一段 ai,然后把它放到最前面,其他段任意分。关于 ai的这个子状态会被重复考虑多次——这个状态是不必要的。

即当我们考虑数组 A中的某一段时,我们只关心两个信息。

  • 能不能选择这一段,即这一段中有没有和之前选择的段重叠了。
  • 能不能放到数组 B的某一段,即 某一段有没有和之前放置的部分重叠了。

由此可以设dp[i][j]表示将 A中的 i部分( 0/1表示)放到 B中的 j部分的最小代价。但这个状态数高达 O(22n),且可能会包含很多非法状态(可能A中某连续部分放不到B某连续部分上),得转换一下状态。

为保持上面的连续性,可以规定将A中的 i部分放到 B中的最前面,即前 popcount(i)(二进制下1的个数)位。即设 dp[i]表示将 A中的 i部分放到 B中的最前面的最小代价 ,记l=popcount(i),即放到 B中的前 l位。

考虑往后转移,即选择i中一段连续的0,然后放到 B[l+1,...]

当然也可以考虑从前转移,但计算代价部分需要O(n3)预处理一下操作二代价,否则转移会多出一个O(n) 复杂度。而往后转移的代价可以在迭代更新维护。

初始情况即dp[0]=0,其余无限大。注意从 0往后转移时没有代价 C。其余的则有。

关于其时间复杂度,初看可能认为是O(n22n),但由于转移时枚举连续的段,其复杂度可能会比这小。分别考虑总状态数和总转移数的话,其实是 O(n2n)

  • 总状态数是O(2n)
  • 总转移数是O(n2n)。注意到每次转移都是由一个连续的段产生的,考虑这些连续的段的转移次数。一个长度为 k的连续的段有(nk+1),能选择该段的状态数有 2nk,由此 总的转移次数为i=1n(nk+1)2nk=2n(n3)+2=O(n2n)
    由此总的时间复杂度就是总状态数+总转移数,即O(2n+n2n)=O(n2n)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
LL c;
cin >> n >> c;
vector<LL> a(n), b(n);
for (auto& i : a)
cin >> i;
for (auto& i : b)
cin >> i;
size_t up = (1 << n);
vector<LL> dp(up, numeric_limits<LL>::max());
dp[0] = 0;
for (size_t i = 0; i < up; ++i) {
int cnt = popcount(i);
for (int j = 0; j < n; ++j) {
if ((i >> j) & 1)
continue;
LL sum = c * (i != 0);
LL nxt = 0;
for (int k = j; k < n; ++k) {
if ((i >> k) & 1)
break;
nxt |= (1 << k);
sum += abs(a[k] - b[cnt + k - j]);
dp[i | nxt] = min(dp[i | nxt], dp[i] + sum);
}
}
}
cout << dp.back() << '\n';
return 0;
}


本文作者:~Lanly~

本文链接:https://www.cnblogs.com/Lanly/p/17826500.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   ~Lanly~  阅读(682)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.