2024 暑假友谊赛 4
2024 暑假友谊赛 4
前言
所有的 Z 类型属于自动取模模板,由于太长就不放了。
A - 加减图
思路
考虑没有操作的时候,\(in_i,out_i\) 代表 \(i\) 的入度和出度,通过拓扑dp可计算出每个点作为终点的简单路径数,设 \(dp1_i\) 表示以 \(i\) 为终点的简单路径数,那么 \(ans=\sum\limits_{i=1}^ndp1_i[out_i=0]\)。
同样的,如果将单向边反过来存,再跑一次拓扑dp,我们可以得到每个点作为起点的简单路径数,设 \(dp2_i\) 表示以 \(i\) 为起点的简单路径数。
考虑一条边 \(x\to y\) 对答案带来的贡献,那么通过分别有 \(dp1_x\) 条路以 \(x\) 为终点,又有 \(dp2_y\) 条路以 \(y\) 为起点,那么这条边就能组合出 \(dp1_x \times dp2_y\) 种简单路径,也代表其对答案产生的贡献。
特别的,如果删除这条边后,\(x,y\) 成为了终点或者起点也会产生新的贡献,需要单独加上贡献也就是 \(dp1_x,dp2_y\),增加这条边使 \(x,y\) 不再成为终点或起点也需要减去相应的贡献。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
constexpr i64 mod = 1000000007;
struct toposort {
vector<vector<int>> e;
vector<int> tp , din;
vector<i64> dp;
int n ;
toposort() {}
toposort(int n) {
this->n = n;
din.resize(n + 1);
e.resize(n + 1);
dp.resize(n + 1);
}
void add(int u, int v) {
e[u].push_back(v);
din[v] ++;
}
bool topo() {
queue<int> Q;
for (int i = 1; i <= n; i ++) {
if (!din[i]) {
Q.push(i);
dp[i] = 1;
}
}
while (Q.size()) {
auto u = Q.front();
Q.pop();
tp.push_back(u);
for (auto v : e[u]) {
dp[v] = (dp[v] + dp[u]) % mod;
if (!--din[v])
Q.push(v);
}
}
return tp.size() == n;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, k;
cin >> n >> m >> k;
toposort t1(n), t2(n);
vector<int> in(n + 1), out(n + 1);
for (int u, v; m; m--) {
cin >> u >> v;
t1.add(u, v);
t2.add(v, u);
out[u] ++, in[v] ++;
}
t1.topo();
t2.topo();
auto dp1 = t1.dp, dp2 = t2.dp;
i64 ans = 0;
for (int i = 1; i <= n; i ++) {
if (!out[i]) {
ans = (ans + dp1[i]) % mod;
}
}
cout << ans << '\n';
for (int op, x, y; k; k--) {
cin >> op >> x >> y;
i64 res = 0;
if (op == 1) {
res = (ans - dp1[x] * dp2[y] % mod + mod) % mod;
if (in[y] == 1) res = (res + dp2[y]) % mod;
if (out[x] == 1) res = (res + dp1[x]) % mod;
} else {
res = (ans + dp1[x] * dp2[y] % mod) % mod;
if (in[y] == 0) res = (res - dp2[y] + mod) % mod;
if (out[x] == 0) res = (res - dp1[x] + mod) % mod;
}
cout << res << '\n';
}
return 0;
}
B - 平方序列
思路
考虑 \(m=1\) 的时候,有 \(1,4,9,16,25,\dots\)
\(m=2\) 的时候,用 \(a\) 代表以上序列,对 \(a\) 序列进行差分得到 \(a_1,a_2-a_1,a_3-a_2,a_4,-a_3,\dots\) 发现只有前面两项组成的子数组满足要求,第二项和第三项会差 \(a_1\),那我们直接加上使得第三项变成 \(a_3+a_1-a_2\),这样第三项和第二项加起来就是 \(a_3\),同理,第四项加第三项又会缺个 \(a_2-a_1\),又加上第二项即可。
综上,对原平方序列差分后每一项加上 \(i-m\) 项即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<i64> a(n + 1);
for (i64 i = 1 ; i <= n; i ++) {
a[i] = i * i - (i - 1) * (i - 1);
}
for (int i = m; i <= n; i ++) {
a[i] += a[i - m];
}
for (int i = 1; i <= n; i ++) {
cout << a[i] << " \n"[i == n];
}
return 0;
}
C - 序列转移
思路
由这个式子 \(\sum(\overline{a}-a_i)^2\) 可知,\(a_i\) 离平均数越近,最后的答案越小,所以我们要使得最后的 \(a_i\) 接近平均数。
又因为不限制操作的次数,所以对于 \(a_i\) ,它前面的 \(a_1\sim a_{i-1}\) 都可以为它 \(+1\),所以一定得把左边大的数转移给右边小的才能使数更均匀,所以也一定存在一种答案序列,其一定是单调不降的,例如样例 \(1\) ,\([2,2,2,6,6,6]\),样例 \(2\),\([9,9,9,9,15,15,15,16,16,17]\)。
具体的,我们可以用单调栈去维护这个东西,维护两个值,将平均数相同的看成一整块,维护这个块的总和和长度(其实也就是平均值,只不过平均值可能为小数,分成分子分母来维护),如果当前块的平均值小于栈顶的,那我们就可以让它和之前的块融合,维护其单调性。
最后栈里的就是我们维护好的平均数的块,由于不能存在小数,所以有的平均数会有多出来 \(+1\) 的情况,注意下即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
Z sum = 0;
vector<int> a(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i];
sum += a[i];
}
Z Avg = sum / Z(n);
stack<array<i64, 2>> st;
for (int i = 1; i <= n; i ++) {
i64 sum = a[i], len = 1;
while (st.size() && sum * st.top()[1] < st.top()[0] * len) {
sum += st.top()[0];
len += st.top()[1];
st.pop();
}
st.push({sum, len});
}
Z ans = 0;
while (st.size()) {
auto [sum, len] = st.top();
st.pop();
i64 avg = sum / len, k = sum % len;
for (int i = 0; i < k; i ++) {
ans += (Avg - (avg + 1)) * (Avg - (avg + 1));
}
for (int i = k; i < len; i ++) {
ans += (Avg - avg) * (Avg - avg);
}
}
ans /= Z(n);
cout << ans << '\n';
return 0;
}
D - 石子交换
思路
按从小到大,其实就是每个数都要和它右边小的进行一次交换,所以可以开两个权值树状数组,一个维护和(\(S\)),一个维护右边比它小的有多少个(\(T\)),对于 \(a_i\),来说,它产生的贡献就是 \(a_i\times \sum\limits_{j=1}^{a_i-1}T_j+\sum\limits_{j=1}^{a_i-1}S_j.\)
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
template<typename T>
struct BIT {
int n;
vector<T> w;
BIT() {}
BIT(int n) {
this->n = n;
w.resize(n + 1);
}
void update(int x, int k) {
for (; x <= n; x += x & -x) {
w[x] += k;
}
}
T ask(int x) {
T ans = 0;
for (; x; x -= x & -x) {
ans += w[x];
}
return ans;
}
T ask(int x, int y) {
return ask(y) - ask(x - 1);
}
};
constexpr int N = 1e7 + 10;
BIT<i64> bit1(N), bit2(N);
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<i64> a(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i];
bit1.update(a[i], a[i]);
bit2.update(a[i], 1);
}
i64 ans = 0;
for (int i = 1; i <= n; i ++) {
ans += bit2.ask(a[i] - 1) * a[i] + bit1.ask(a[i] - 1);
bit1.update(a[i], -a[i]);
bit2.update(a[i], -1);
}
cout << ans << '\n';
return 0;
}
E - 信鸽
思路
要使得整体最快,那就应该让最晚的最快,所以我们可以二分答案。
具体的,对于大于二分答案 \(mid\) 的线段,对其左右区间取交集,最后统一判断去掉这些交集后的线段是否还大于 \(mid\) 即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
int mx = 0;
vector<array<int, 2>> a(m);
for (auto &[l, r] : a) {
cin >> l >> r;
mx = max(mx, r - l);
}
auto check = [&](int mid)-> bool{
int x = 0, y = n;
for (auto [l, r] : a) {
if (r - l > mid) {
x = max(x, l);
y = min(y, r);
}
}
int len = max(0, y - x);
for (auto [l, r] : a) {
if (r - l - len > mid) {
return false;
}
}
return true;
};
int l = 0, r = mx, ans = mx;
while (l <= r) {
int mid = l + r >> 1;
if (check(mid)) r = mid - 1, ans = mid;
else l = mid + 1;
}
cout << ans << '\n';
return 0;
}
F - Boss 战
思路
可以把 \(a_i\) 看成从当前点能到达的长度,假如当前点为 \(m\),那么它下一个点就是 \(m-a_i\),而血量为 \(m-a_i\) 的 boss 的攻击值 \(b_{m-a_i}\) 我们可以看做这条边的边权。
综上,我们就抽象出了一个有向的权值图,要求承受伤害最小,也就是边权最小,那么直接对这个图跑最短路 Dijkstra 即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
struct DIJ {
using i64 = long long;
using PII = pair<i64, i64>;
vector<i64> dis;
vector<vector<PII>> G;
DIJ() {}
DIJ(int n) {
dis.assign(n + 1, 1e18);
G.resize(n + 1);
}
void add(int u, int v, int w) {
G[u].emplace_back(v, w);
}
void dijkstra(int s) {
priority_queue<PII, vector<PII>, greater<PII>> que;
dis[s] = 0;
que.push({0, s});
while (!que.empty()) {
auto p = que.top();
que.pop();
int u = p.second;
if (dis[u] < p.first) continue;
for (auto [v, w] : G[u]) {
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
que.push({dis[v], v});
}
}
}
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<int> a(n + 1), b(m + 1);
for (int i = 1 ; i <= n; i ++) {
cin >> a[i];
}
for (int i = 1; i <= m; i ++) {
cin >> b[i];
}
DIJ dij(m);
for (int i = 1; i <= n; i ++) {
for (int j = m; j >= 1; j --) {
int to = max(0, j - a[i]);
dij.add(j, to, b[to]);
}
}
dij.dijkstra(m);
cout << dij.dis[0] << '\n';
return 0;
}
G - alphabets & digits
思路
最终的答案无非就是数字在左边,字母在右边,又或者数字在右边,字母在左边。
所以我们可以直接先统计两者的数量,然后枚举两种情况,看谁更优即可。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
string s;
cin >> s;
int a = 0, b = 0;
for (auto c : s) {
if (isdigit(c)) a ++;
else b ++;
}
int ans = s.size();
int d = 0;
for (int i = 0; i < a; i ++) {
if (!isdigit(s[i])) {
d ++;
}
}
ans = min(ans, d);
d = 0 ;
for (int i = 0; i < b; i ++) {
if (isdigit(s[i])) {
d ++;
}
}
ans = min(ans, d);
cout << ans << '\n';
return 0;
}
I - 乱序开题
思路
按正常思路模拟即可,但是要注意的是,题目给的是通过每一题时的时间,而不是每一题用了多少时间,所以我们需要反推用解决该题时的时间减去解决上一题时的时间得到单独解决这题花费的时间(题目说明只会解决完一道题之后才会解决下一道)。
代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for (auto &i : a)
cin >> i;
int sum = 0, ans = 0;
for (auto i : a) {
int x = i;
for (auto j : a) {
if (j < i) {
x = min(x, i - j);
}
}
sum += x;
ans += sum;
}
while (n--) {
int x;
cin >> x;
ans += x * 20;
}
cout << ans << '\n';
return 0;
}