AtCoder Beginner Contest 282
A - Generalized ABC (abc282 a)
题目大意
给定\(n\),输出一个字符串,从'A','B','C'
...拼接得到的长度为 \(n\)。
解题思路
模拟即可。
神奇的代码
#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;
string s;
for(int i = 0; i < n; ++ i)
s += 'A' + i;
cout << s << '\n';
return 0;
}
B - Let's Get a Perfect Score (abc282 b)
题目大意
给定一个\(01\)矩阵。选两行,其每一列至少有一个 \(1\)。问满足要求的选法。
解题思路
范围不大,暴力即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, m;
cin >> n >> m;
vector<string> qwq(n);
for(auto &i : qwq)
cin >> i;
int ans = 0;
auto check = [&](int x, int y){
int cnt = 0;
FOR(i, 0, m){
cnt += (qwq[x][i] == 'o' || qwq[y][i] == 'o');
}
return cnt == m;
};
FOR(i, 0, n)
FOR(j, i + 1, n){
ans += check(i, j);
}
cout << ans << '\n';
return 0;
}
C - String Delimiter (abc282 c)
题目大意
给定一个字符串,要求把非"
括起来的,
换成.
。
解题思路
模拟即可。
神奇的代码
#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;
string s;
cin >> n >> s;
bool ok = false;
for(auto &i : s){
if (i == '\"')
ok ^= 1;
else if (i == ',' && !ok)
i = '.';
}
cout << s << '\n';
return 0;
}
D - Make Bipartite 2 (abc282 d)
题目大意
给定一张\(n\)个点的无向图,给其加一条边,满足其是二分图。问满足该条件的方案数。
解题思路
注意原图不一定连通。
对原图进行黑白染色,如果颜色冲突则答案为\(0\)。
否则,对于一个连通块内,假设点数为\(cnt\),边数为 \(cntm\),染了\(w\)个白点和 \(b\)个黑点,则满足要求的方案数为 \(wb - cntm\)。该连通块还能连向其他连通块,且可以任意连,其方案数为\(cnt \times (n - cnt)\)。
对所有连通块累计方案数即为答案。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, m;
cin >> n >> m;
vector<vector<int>> edge(n + 1);
vector<int> deep(n + 1, -1);
FOR(i, 0, m){
int u, v;
cin >> u >> v;
edge[u].push_back(v);
edge[v].push_back(u);
}
LL ans = 0;
vector<int> col(2, 0);
bool ok = true;
int cnt = 0;
set<pair<int, int>> ee;
function<void(int, int, int)> dfs = [&](int u, int fa, int cc){
deep[u] = cc;
col[cc] ++;
++ cnt;
for(auto &v : edge[u]){
if (v == fa)
continue;
ee.insert({u, v});
ee.insert({v, u});
if (deep[v] != -1){
if (deep[v] == deep[u])
ok = false;
}else
dfs(v, u, cc ^ 1);
}
};
int block = 0;
FOR(i, 1, n + 1){
if (deep[i] == -1){
++ block;
cnt = 0;
col[0] = col[1] = 0;
ee.clear();
dfs(i, i, 0);
int cntm = ee.size() / 2;
ans += 1ll * col[0] * col[1] - cntm;
ans += 1ll * cnt * (n - cnt);
n -= cnt;
}
}
if (!ok)
ans = 0;
cout << ans << '\n';
return 0;
}
E - Choose Two and Eat One (abc282 e)
题目大意
给定\(n\)个数,每个数范围\([1, m - 1]\),每次选择两个数\(x,y\),获得 \(x^y + y^x \mod m\) 分数。再将其中一个数丢弃,直至剩一个数。问获得的分数最大值。
解题思路
对于每个数,可能被选择多次,其中仅一次会被干掉,其他的都能保留。一个与多个的关系与树节点的一个父亲和多个儿子非常类似。
给定一棵树,题意操作相当于每次将叶子节点去掉,同时获得叶子与父亲的边权值。
对原图跑一边最大生成树即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)
int n, m;
LL qpower(LL a, LL b){
LL qwq = 1;
while(b){
if (b & 1)
qwq = qwq * a % m;
a = a * a % m;
b >>= 1;
}
return qwq;
}
LL calc(LL x, LL y){
return (qpower(x, y) + qpower(y, x)) % m;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m;
vector<vector<LL>> dis(n + 1, vector<LL>(n + 1, 0));
vector<LL> a(n + 1);
for(int i = 1; i <= n; ++ i){
cin >> a[i];
}
vector<pair<LL, pair<int, int>>> edge;
for(int i = 1; i <= n; ++ i)
for(int j = 1; j <= n; ++ j)
edge.push_back({calc(a[i], a[j]), {i, j}});
vector<int> fa(n + 1);
function<int(int)> findfa = [&](int x){
return fa[x] == x ? x : fa[x] = findfa(fa[x]);
};
iota(fa.begin(), fa.end(), 0);
sort(edge.begin(), edge.end(), [&](const pair<LL, pair<int, int>>& a, const pair<LL, pair<int, int>>& b){
return a.first > b.first;
});
LL ans = 0;
for(auto &i : edge){
int fu = findfa(i.second.first);
int fv = findfa(i.second.second);
if (fu != fv){
fa[fu] = fv;
ans += i.first;
}
}
cout << ans << '\n';
return 0;
}
F - Union of Two Sets (abc282 f)
题目大意
交互题。给定\(n\),要求生成 \(m\)个区间,并用这些区间回答 \(q\)组询问。
每组询问给定 \(l,r\),要求从 \(m\)个区间选出 \(2\)个区间,其并集为区间\([l,r]\) 。
\(n \leq 4000, m \leq 500000\)
解题思路
其实\(RMQ\)里的\(ST\)表的做法就能达到题目所述的要求。
即为每个左端点生成长度为\(2\)的幂的区间。
设长度 \(len = r - l + 1\) ,其最大的小于等于\(len\)的 \(2\)的幂为 \(x\)。
则选择的区间为 \([l, l + x - 1], [r - x + 1, r]\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
#define FOR(i, x, y) for (decay<decltype(y)>::type i = (x), _##i = (y); i < _##i; ++i)
#define FORD(i, x, y) for (decay<decltype(x)>::type i = (x), _##i = (y); i > _##i; --i)
inline int highbit(int x) { return 31 - __builtin_clz(x); }
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n;
cin >> n;
map<pair<int, int>, int> area;
int m = 0;
for(int i = 1; i <= n; ++ i){
for(int j = 1; i + j - 1 <= n; j <<= 1){
++ m;
area[{i, i + j - 1}] = m;
}
}
cout << area.size() << '\n';
for(auto &i : area){
cout << i.first.first << ' ' << i.first.second << '\n';
}
cout.flush();
int q;
cin >> q;
while(q--){
int l, r;
cin >> l >> r;
int t = highbit(r - l + 1);
cout << area[{l, l + (1 << t) - 1}] << ' ' << area[{r - (1 << t) + 1, r}] << endl;
}
return 0;
}
G - Similar Permutation (abc282 g)
题目大意
定义两个排列的相似度为,差分数组下符号相同的标号数量。
给定\(n,k,p\),问 长度为 \(n\)的相似度为 \(k\)的排列对数,对\(p\)取模。
解题思路
起初想的是从小到大插入每个数统计方案,但是一旦插入到前面的位置,就会影响到后面的相似度判断,需要记录前面的升降趋势状态数会非常大,该方向不可行。
然后考虑按一位一位填数。要知道该位填的数与上一位关系的大小关系,需要记录上一位数是多少,但这样的话也得记录目前已经填了哪些数,这样才能知道有多少种填法是小于上一个数。而已经填了哪些数这一状态是指数级的。局面一度陷入僵局。
细细一想,我们需要的状态:上一个数填的是什么,以及当前填了哪些数,是为了决定最后两个数字的大小关系(这关系到相似度是否\(+1\)),而并不关心这两个数具体是多少,我们只想知道,有多少种填法是小于,有多少种填法是大于。
因此我们可以设计的状态是:当前已填的最后一个数在未填的数中排名第几位。通过这一状态,我们就知道:有多少种填法是小于,有多少种填法是大于。
即\(dp[i][j][a][b]\)表示我们填了 \(i\)个数,两个排列的相似度为 \(j\), 第一个排列的最后一个数在未填数中排名为\(a\),第二个排列的最后一个数在未填数中排名为 \(b\)的方案数。
则
即新填一个数
- 相似度不增加,则是一小一大和一大一小两种情况
- 相似度加一,则是一小一小和一大一大两种情况
朴素转移是\(O(n^2)\),可以用二维前缀和优化成 \(O(1)\),因此总的时间复杂度为 \(O(n^4)\)
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 100 + 8;
int n, k, p, cur;
int dp[2][N][N][N], sum[N][N];
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> k >> p;
cur = 0;
for(int i = 1; i <= n; ++ i)
for(int j = 1; j <= n; ++ j)
dp[cur][0][i][j] = 1;
for(int i = 2; i <= n; ++ i){
cur ^= 1;
memset(dp[cur], 0, sizeof(dp[cur]));
for(int j = 0; j <= k; ++ j){
int up = n - i + 2; // 未填数数量+1,即排名的最大值
for(int a = 1; a <= up; ++ a)
for(int b = 1; b <= up; ++ b){
sum[a][b] = (0ll + sum[a - 1][b] + sum[a][b - 1] - sum[a - 1][b - 1] + dp[cur ^ 1][j][a][b]) % p;
if (sum[a][b] < 0)
sum[a][b] += p;
}
for(int a = 1; a <= up; ++ a)
for(int b = 1; b <= up; ++ b){
dp[cur][j][a][b] = (0ll + dp[cur][j][a][b] + sum[a][up] - sum[a][b] + sum[up][b] - sum[a][b]) % p;
if (dp[cur][j][a][b] < 0)
dp[cur][j][a][b] += p;
dp[cur][j + 1][a][b] = (0ll + dp[cur][j + 1][a][b] + sum[a][b] + sum[up][up] - sum[a][up] - sum[up][b] + sum[a][b]) % p;
if (dp[cur][j + 1][a][b] < 0)
dp[cur][j + 1][a][b] += p;
}
}
}
cout << dp[cur][k][1][1] << '\n';
return 0;
}
Ex - Min + Sum (abc282 h)
题目大意
给定两个长度为\(n\)的数组\(A,B\)和一个整数\(s\)。问有多少对 \((l,r)\),满足
解题思路
条件有两项,我们先考虑第一项。
考虑\(a[i]\)作为其区间的最小值,那么该区间 \([l,r]\)需要满足一定条件。
用单调栈预处理 \(L[i],R[i]\)数组分别表示 \(a[i]\)左边第一个(最靠近的)小于等于 \(a[i]\)的下标,以及\(a[i]\)右边第一个小于 \(a[i]\)的下标, 那么满足\(L[i] < l \leq i \leq r < R[i]\)的区间 \([l,r]\)的最小值都是 \(a[i]\),这样就确定好了第一项。
考虑和式,首先通过前缀和将和式变成两数相减的形式。因为前缀和是单调递增的,如果我们枚举\(l\),通过二分可以找到符合条件的 \(r\),同理枚举 \(r\),通过二分也能找到符合条件的 \(l\)。
看\(l\)和 \(r\)的枚举个数(即\(i - l\)和 \(r - i\)的大小),取较小的那边枚举二分求解即可。咋一看这做法的复杂度可能有\(O(n^2\log n)\)的量级,但可以证明这样的复杂度其实是\(O(n\log^2 n)\)
考虑全局最小值,其枚举范围是整个数组。考虑完后该最小值将整个数组切分成两部分,然后递归考虑左边的最小值和右边的最小值。这里有分治的想法,这里我们主要关注合并。
考察每个端点被枚举的次数。因为它所在的区间是长度较小的,当枚举完后,下一个枚举到它的区间的长度(左右合并成一个大区间)至少翻倍,最多翻倍\(\log\)次就到长度为 \(n\)了。所以每个端点被枚举的次数是 \(\log\)次,加上每次枚举的二分有一个\(\log\)。因此总的时间复杂度为 \(O(n \log^2 n)\)
神奇的代码
#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 s;
cin >> n >> s;
vector<LL> a(n + 1), b(n + 1);
for(int i = 1; i <= n; ++ i)
cin >> a[i];
for(int i = 1; i <= n; ++ i)
cin >> b[i];
vector<LL> sumb(n + 1);
partial_sum(b.begin() + 1, b.end(), sumb.begin() + 1);
vector<int> l(n + 1), r(n + 1);
stack<int> pos;
for(int i = 1; i <= n; ++ i){
while(!pos.empty() && a[pos.top()] >= a[i]){
r[pos.top()] = i - 1;
pos.pop();
}
pos.push(i);
}
while(!pos.empty()){
r[pos.top()] = n;
pos.pop();
}
for(int i = n; i >= 1; -- i){
while(!pos.empty() && a[pos.top()] > a[i]){
l[pos.top()] = i + 1;
pos.pop();
}
pos.push(i);
}
while(!pos.empty()){
l[pos.top()] = 1;
pos.pop();
}
auto solve = [&](int l, int mid, int r){
LL cnt = 0;
int len1 = mid - l + 1;
int len2 = r - mid + 1;
vector<LL>::iterator sb = sumb.begin(), st = sb + l - 1, midd = sb + mid, en = sb + r + 1;
LL up = s - a[mid];
if (len1 < len2){
for(auto it = st; it != midd; it = next(it)){
auto pos = upper_bound(midd, en, up + *it);
cnt += pos - midd;
}
}else{
for(auto it = midd; it != en; it = next(it)){
auto pos = lower_bound(st, midd, *it - up);
cnt += midd - pos;
}
}
return cnt;
};
LL ans = 0;
for(int i = 1; i <= n; ++ i){
ans += solve(l[i], i, r[i]);
}
cout << ans << '\n';
return 0;
}