另一种阅读体验:https://zhuanlan.zhihu.com/p/1895276194334766227
争取在省赛前做到每周至少更五篇博客
因为是新手向,所以我会尽可能写的详细点(
前言
本文的题解代码均为solve()
单独函数,基于以下代码为基础,这一块完全可以掠过不看
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
typedef std::pair<long long, long long> PII;
const int mod = 998244353;
const int N = 2e6 + 1000;
const int INF = 0x3f3f3f3f;
const long long LINF = 1e18;
const double eps = 1e-12;
const double pi = std::acos(-1);
std::mt19937_64 rnd(std::chrono::steady_clock::now().time_since_epoch().count());
std::uniform_int_distribution<u64> dist_rand(mod / 2, mod - 1);
void solve() {
}
signed main() {
std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
int tmp = 1;
std::cin >> tmp;
while (tmp--)
solve();
return 0;
}
可分解的正整数
思路
当然这题有很多神秘的数学解法,但关键在于负数也可以参与到序列中,那么有
x=∑i=−(x−1)0i+∑i=0x−1i+x
x = \sum_{i=-(x-1)}^0 i + \sum_{i=0}^{x - 1} i + x
x=i=−(x−1)∑0i+i=0∑x−1i+x
其中∑\sum∑表示累加,例如∑i=13i=1+2+3\sum_{i = 1}^3 i = 1 + 2+ 3∑i=13i=1+2+3
−(x−1)+−(x−2)+(−2)+(−1)⋯+0+1+2⋯+(x−2)+(x−1)=0-(x - 1) + -(x -2) + (-2) + (-1)\dots + 0 + 1 + 2 \dots + (x-2) + (x-1) = 0−(x−1)+−(x−2)+(−2)+(−1)⋯+0+1+2⋯+(x−2)+(x−1)=0
最后加上xxx即可完成我们的序列
事实上当x>1x > 1x>1时都可以满足要求,因为$x = 2 时有时有时有[-1, 0, 1, 2]$,此时长度大于333,比222大的数的序列只会更大
考虑边界情况x=1x = 1x=1,此时是无法组成合法序列的
所以答案是数组中元素值不等于111的个数
代码
void solve() {
int n, ans = 0;
std::cin >> n;
for (int i = 1; i <= n; i++) {
int x;
std::cin >> x;
if (x != 1) ans ++;
}
std::cout << ans << '\n';
}
产值调整
思路
正解与暴力的区别在于if (a == A && b == B && c == C) break;
的一个特判
通过打表我们可以发现执行次数其实最多执行log\loglog级别的次数,每次迭代都会使三个数逐渐趋于相同,所以只需要判断下什么时候迭代不会对三个数产生影响就可以
时间复杂度是O(tlog(A))O(t \log(A))O(tlog(A)),其中AAA是a,b,ca,b,ca,b,c的值域上限
代码
void solve() {
int a, b, c, k;
std::cin >> a >> b >> c >> k;
for (int i = 1; i <= k; i++) {
int A = (b + c) / 2;
int B = (a + c) / 2;
int C = (a + b) / 2;
if (a == A && b == B && c == C) break;
a = A, b = B, c = C;
}
std::cout << a << ' ' << b << ' ' << c << '\n';
}
画展布置
思路
看到∑i=lr∣ai+1−ai∣\sum_{i=l}^r |a_{i+1} - a_i|∑i=lr∣ai+1−ai∣类型的题目,我们应该往排序上思考
因为排序后可以将绝对值去掉,例如我们可以先对数组升序排序,这样子确保ai+1≥aia_{i + 1} \ge a_iai+1≥ai,这样子式子变成了∑i=lrai+1−ai\sum_{i=l}^r a_{i+1} - a_i∑i=lrai+1−ai,更有助于我们分析题目
同样适用于本题,鉴于本题并不要求元素之间的顺序关系,所以我们可以放心排序,并且如果满足a>b>0a > b > 0a>b>0,那么一定有a2>b2>0a^2 > b^2 > 0a2>b2>0,所以我们可以先对每个元素进行平方后再排序
因为我们希望让∑i=lrai+1−ai\sum_{i=l}^r a_{i+1} - a_i∑i=lrai+1−ai尽可能小,实际上在排序后就是让我们选择一个长度为mmm的区间,让这个区间中相互元素差的和尽可能小。如果觉得这里毕竟晦涩难懂,那么我推荐你可以画一个二维坐标系,通过数形结合的方式进行理解
当然我们也可以简单的证明一下,假设有三个数a1,a2,a3(a1<a2<a3)a_1, a_2,a_3(a_1 < a_2 <a_3)a1,a2,a3(a1<a2<a3),想让元素差尽可能小一定要相邻的数,假设我们选两个数,那么有三种情况(a1,a2),(a2,a3),(a1,a3)(a_1, a_2), (a_2, a_3), (a_1, a_3)(a1,a2),(a2,a3),(a1,a3),差值分别是a2−a1,a3−a2,a3−a1=(a3−a2)+(a2−a1)a_2 - a_1, a_3 - a_2, a_3 - a_1 = (a_3 -a_2) + (a_2 - a_1)a2−a1,a3−a2,a3−a1=(a3−a2)+(a2−a1),显然最后这个差比前两个差只会更高
代码
void solve() {
int n, m;
std::cin >> n >> m;
std::vector<i64> a(n + 1);
for (int i = 1; i <= n; i++) std::cin >> a[i], a[i] *= a[i];
std::sort(a.begin(), a.end());
i64 ans, sum = 0;
for (int i = 2; i <= m; i ++) sum += a[i] - a[i - 1];
ans = sum;
for (int i = m + 1; i <= n; i++) {
sum -= a[i - m + 1] - a[i - m];
sum += a[i] - a[i - 1];
ans = std::min(ans, sum);
}
std::cout << ans << "\n";
}
水质检测
思路
一道很经典的dp题
对于dp题我们都可以直接思考dp的三组件——定义,初始化,转移
解决完这三组件那么这道dp也随之解决了
定义:定义dpi,jdp_{i, j}dpi,j为使得000行和111行的[1,j−1][1, j - 1][1,j−1]列区间的检测器都联通的条件下,让(i,j)(i,j)(i,j)这个地方也变成探测器(即让这个点也联通)的最小花费
初始化:因为字符串的两端有可能都没有探测器,所以是没必要处理这两段的,所以我们可以通过
int l = 1, r = n;
while (c[0][l] + c[1][l] == 2) l ++;
while (c[0][r] + c[1][r] == 2) r --;
来修改我们要dp的范围,使得我们的dp定义合法
这里的ci,jc_{i, j}ci,j表示第(i,j)(i, j)(i,j)位置是否无探测器,是...为111,否则为000
而关于最初的第一列,三种情况:
1 # | 2 # | 3 .
# | . | #
对于第111种情况,两个点都已经有探测器,显然有dp0/1,1=0dp_{0/1, 1} = 0dp0/1,1=0
对于第222种情况,第000行第111列的点有探测器,第111行第111列的点无探测器,那么有dp0,1=0,dp1,1=1dp_{0,1} = 0, dp_{1,1}= 1dp0,1=0,dp1,1=1,因为想让第(1,1)(1, 1)(1,1)位置联通,我们需要在这个位置上放一个探测器
第333种情况和第222种情况同理
实际上可以简化成dpi,1=ci,jdp_{i, 1} = c_{i, j}dpi,1=ci,j
转移:对于第(i,j)(i, j)(i,j)个点的转移也只有三种情况
1
(i, j - 1) -> (i, j)
(i + 1, j - 1) (i + 1, j)
—————————————————————————————————————————————
2
(i, j - 1) -> (i, j)
↑
(i + 1, j - 1) (i + 1, j)
——————————————————————————————————————————————
3
(i, j - 1) (i, j)
↑
(i + 1, j - 1) -> (i + 1, j)
实际上上面的(i+1)(i + 1)(i+1)应该是(i⊕1)(i\oplus 1)(i⊕1),为了便于理解这里让iii是第000行
对于第一种情况,转移为dpi,j←dpi,j−1+ci,jdp_{i, j} \gets dp_{i, j -1} + c_{i, j}dpi,j←dpi,j−1+ci,j,后面的+ci,j+c_{i, j}+ci,j是如果第(i,j)(i, j)(i,j)没有探测器的话需要花费111的代价添加
第二种情况,转移为dpi,j←dpi⊕1,j−1+ci,j−1+ci,jdp_{i, j} \gets dp_{i \oplus 1, j -1} + c_{i, j - 1} + c_{i, j}dpi,j←dpi⊕1,j−1+ci,j−1+ci,j
第三种情况,转移为dpi,j←dpi⊕1,j−1+ci⊕1,j+ci,jdp_{i, j} \gets dp_{i \oplus 1, j -1} + c_{i \oplus 1, j} + c_{i, j}dpi,j←dpi⊕1,j−1+ci⊕1,j+ci,j
取三种情况最小的值进行转移即可
解决完定义,初始化和转移,这道题只需要将上面翻译成代码也就完成了
代码
void solve() {
std::string s1, s2;
std::cin >> s1 >> s2;
int n = s1.size();
std::vector c(2, std::vector<int>(n + 1));
for (int i = 1; i <= n; i ++) c[0][i] = s1[i - 1] == '.';
for (int i = 1; i <= n; i ++) c[1][i] = s2[i - 1] == '.';
std::vector dp(2, std::vector<int>(n + 1));
int l = 1, r = n;
while (c[0][l] + c[1][l] == 2) l ++;
while (c[0][r] + c[1][r] == 2) r --;
if (l > r) {
std::cout << "0\n";
return ;
}
dp[0][l] = c[0][l], dp[1][l] = c[1][l];
for (int i = l + 1; i <= r; i ++)
for (int j = 0; j < 2; j ++)
dp[j][i] = std::min({dp[j][i - 1], dp[j ^ 1][i - 1] + c[j][i - 1], dp[j ^ 1][i - 1] + c[j ^ 1][i]}) + c[j][i];
std::cout << std::min(dp[0][r], dp[1][r]) << '\n';
}
生产车间
思路
实际上这道题才是本次省赛最难的题目
一句话题解:树上跑可行性01背包用bitset优化
因为这题已经很不新手向了,所以这题不会像上一题写的那么详细
那么同样考虑dp的三组件——定义,初始化,转移
定义dpu,sdp_{u, s}dpu,s,表示以uuu为根的子树是否可以打包值为sss的数量,uuu节点可以向上打包的值的集合由一串二进制maskmaskmask表示,如果maski=truemask_i = truemaski=true表示可以打包值为iii,否则不可以
考虑初始化,对于每个节点,显然可以把其子树所有节点都删掉,那么也就是打包的值为000,即dpu,0=truedp_{u, 0} = truedpu,0=true;对于叶子节点,因为其可以自己生成,所以有dpu,au=truedp_{u, a_u} = truedpu,au=true
考虑转移,对于节点uuu和他的儿子节点vvv,由于每个节点只能选择一个值进行打包,所以这里其实还是个分组的01背包,所以先令tmp=dputmp = dp_utmp=dpu,然后用tmptmptmp去对dpudp_udpu进行转移
如果直接跑010101背包的话时间复杂度为O(nw2)O(nw^2)O(nw2),其中nnn是枚举每个点,w2w^2w2是对分组背包的每个值都进行一次01背包,这样会超出我们时间限制
不过01背包有个很经典的bitset优化,可以使我们的时间复杂度优化到O(nw264)O(\frac {nw^2}{64})O(64nw2)的程度,这样就可以通过本题了
关于转移,枚举当前要选择vvv打包的值,当dpv,i=truedp_{v, i} = truedpv,i=true时,有dpu←dpu∣(tmp<<i)dp_u \gets dp_u |(tmp << i)dpu←dpu∣(tmp<<i)
最后从大到小枚举下根节点能打包的最大值即可
代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; i ++) std::cin >> a[i];
std::vector go(n + 1, std::vector<int>());
for (int i = 1; i < n; i ++) {
int u, v;
std::cin >> u >> v;
go[u].emplace_back(v);
go[v].emplace_back(u);
}
std::vector<std::bitset<1001>> dp(n + 1);
auto dfs = [&](auto &&dfs, int u, int fa) ->void {
dp[u][0] = true;
bool leaf = true;
for (auto v : go[u]) {
if (v == fa) continue;
dfs(dfs, v, u);
auto tmp = dp[u];
for (int i = 1; i <= a[v]; i ++) if (dp[v][i]) dp[u] |= tmp << i;
leaf = false;
}
if (leaf) dp[u][a[u]] = true;
};
dfs(dfs, 1, 1);
for (int i = a[1]; i >= 0; i --) if (dp[1][i]) {
std::cout << i << "\n";
return ;
};
}
装修报价
思路
代码中的Z
变量类型是一个自动取模的变量类型
实际上这道题是一道诈骗题,因为有
x=a1+(a2+−⊕a3… )
x = a_1 + (a_2 +-\oplus a_3 \dots)
x=a1+(a2+−⊕a3…)
y=a1−(a2+−⊕a3… )
y = a_1 - (a_2 +-\oplus a_3 \dots)
y=a1−(a2+−⊕a3…)
x+y=a1+a1
x + y = a_1 + a_1
x+y=a1+a1
实际上几乎只有第一个数有贡献,至于几乎是因为当包含a1a_1a1的异或中会优先计算异或,所以其他数在这时候会有贡献
考虑枚举异或的区间,左端点只能是111,右端点是i(i<n)i(i < n)i(i<n),那么有
ans←∑i=2n−1(⊕j=1iaj)⋅2⋅3n−i−1
ans \gets \sum_{i = 2}^{n - 1} (\oplus_{j = 1}^i a_j) \cdot 2 \cdot 3^{n - i - 1}
ans←i=2∑n−1(⊕j=1iaj)⋅2⋅3n−i−1
这里表示[a1⊕a2⋯⊕ai+...][a_1 \oplus a_2\dots \oplus a_i +...][a1⊕a2⋯⊕ai+...]和[a1⊕a2⋯⊕ai−...][a_1 \oplus a_2\dots \oplus a_i -...][a1⊕a2⋯⊕ai−...],因为aia_iai后面的符合只能是加或减,有两种可能所以是乘二,对于ai+1a_{i+1}ai+1后面的符合都任意,后面有n−i−1n - i - 1n−i−1个符号,所以乘3n−i−13^{n - i - 1}3n−i−1
当右端点为nnn时,后面没有符合了不适用于前面的公式了,但此时也只有全部异或这一种情况,这里单独特判即可
ans←ans+⊕i=1nai
ans \gets ans +\oplus_{i = 1}^n a_i
ans←ans+⊕i=1nai
代码
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; i++) std::cin >> a[i];
i64 x = 0;
Z ans = 0;
for (int i = 1; i <= n; i ++) {
x ^= a[i];
if (i == n) ans += x;
else ans += x * 2 * power(Z(3), n - i - 1);
}
std::cout << ans << "\n";
}