10.8 模拟赛题解
10.8 模拟赛题解
0x01
首先,不得不说,这次模拟赛考察了基础算法,尤其是DFS,我觉得我还有很多不足,尤其是在暴力拿分的部分。
然后由于评测机的问题一开始强行爆零,后来换了台电脑测拿了100pts,至少不是倒一......
0x02
A. stone
题目大意:给定一个单调不降序列,其最大值为 \(k\) , 每次可以使序列中的最小数字变成次小数字,求出使整个序列全部变为 \(k\) 的方案数。
样例:
input :
4
1 1 2 3
output :
5
这道题做法很多,看到求方案数,其实第一反应就应该使用dp来做,实际在考场上写了个假的贪心,死的很惨.
考虑动态规划做法。
设计状态:\(dp[i]\) 表示 第 \(i\) 个数需要调整的次数。
边界条件:\(dp[n] = 0\); 目标 \(dp[i]\).
转移方程:if(a[i+1] > a[i]) dp[i] = dp[i+1] + 1; if(a[i+1] == a[i]) dp[i] = dp[i+1];
通过这个转移方程,我们注意到更新dp数组要从 \(n-1\) 到 1。
为什么要逆序呢?因为很容易发现这个dp的依赖关系永远是较大的元素转移给较小的元素。
\(Code:\)
#include <bits/stdc++.h>
using namespace std;
int n, ans, a[MAXN], dp[MAXN];
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
dp[n] = 0;
for(int i = n-1; i >= 1; i--) {
if(a[i+1] > a[i]) dp[i] = dp[i+1] + 1;
if(a[i+1] == a[i]) dp[i] = dp[i+1];
}
for(int i = 1; i <= n; i++) ans += dp[i];
printf("%d", &ans);
return 0;
}
时间复杂度 \(O(N)\) , 可以通过本题。
B. matrix
题目大意:求一个数字\(k\)在一个蛇形矩阵中具体的位置。已知 \(0\le k\le10^{18}\)
蛇形矩阵是甚么东西呢?
1, 2, 9, 10, ...
4, 3, 8, 11, ...
5, 6, 7, 12, ...
16, 15, 14, 13, ...
大概长这样。
我们把这个蛇形矩阵变得更蛇形一点.....记为蛇形矩阵(2).
1,
4, 3, 2,
5, 6, 7, 8, 9,
16, 15, 14, 13, 12, 11
....
我们观察到一个规律
数字范围 含有数据数
1 | 1
2~4 | 3
5~9 | 5
10~16 | 7
17~25 | 9
规律十分明显,等差数列。
我们对这个等差数列求个前缀和,有 \(pre_i = i ^ 2\).
于是我们想到,第 \(i\) 行的数字取值范围为 \([(i-1)^2+1, i^2]\)
对于一个输入数据 \(k\),我们可以先对它开根号,记录以下两个数据。
a = floor((double)sqrt(k));
b = ceil((double)sqrt(k));
知道 \(b\) 相当于 知道了在图(2)中的行数。
这个时候我们又发现,需要对 \(a^2+1\) 的奇偶性分类讨论:
- 如果 \(a^2+1\) 是奇数,那么在蛇形矩阵(2)中的这一行是从左到右递增的;
- 如果是偶数,那么就是递减的。
为什么会用到这个性质呢?因为要计算 \(k\) 和 图(2) 中每一行起点的差值来计算他在原图中到底是在行上还是在列上。
请仔细理解这句话。之后,代码就呼之欲出了。
\(Code:\)
/*L7 xuzhengyang
思路:根据打表发现一个规律
先算sqrt(k), 然后向下取整,记为a,向上取整,记为b, 则s[a]+1 <= k <= s[b]
注意分奇偶讨论
*/
#include <bits/stdc++.h>
#define ll long long
using namespace std;
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
int t;
ll k;
int main() {
freopen("matrix.in", "r", stdin);
freopen("matrix.out", "w", stdout);
read(t);
while(t--) {
read(k);
ll a = floor((double)sqrt(k));
ll b = ceil((double)sqrt(k));
bool flag = 0;
if(a == b) a--;
if(b & 1) {
if(k - a * a <= b) {
printf("%lld %lld\n", b, k - a * a);
continue;
}
if(k - b * b >= -b){
printf("%lld %lld\n", b * b - k + 1, b);
continue;
}
} else {
if(k - b * b - 1 >= -b) {
printf("%lld %lld\n", b, b * b - k + 1);
continue;
}
if(k - a * a - 1 <= b) {
printf("%lld %lld\n", k - a * a, b);
continue;
}
}
a = 0, b = 0;
}
return 0;
}
C. boxing
不会,先隔这。
UPD:2021.10.9 问了下教练,用dp的方法做出来了。
题目大意:有 \(n\) 个数,每个数字值为 \(a_i\),从其中选出若干个和不超过 \(m\) 的数字的最小公倍数,求有多少种最小公倍数。
数据范围:n, m \(\le\) 120
考场上一看到这样的题面,立马就想到之前做过的摆花这道题,但这道题的难点在于如何转移 lcm。
首先我们设计状态为 dp[i][j][k]
表示是否存在一种方案可以选取前 \(i\) 个数,和为 \(j\),lcm 为 \(k\) ,很显然这是个bool型的dp,为什么要用bool型的dp呢,因为这道题目最后求的是有多少种不同的 \(lcm\) 的选法,然而如果转移lcm(即将状态设计成 dp[i][j] 表示 选前 \(i\) 个数,和为 \(j\) 的 \(lcm\))显然是具有后效性的,不符合动态规划的定义,最简便的办法是把有后效性的部分加进状态方程里,也就得到了我们最开始说的dp状态。我一开始就是没想到这个所以死的很惨.
由摆花的经验我们可以得到这道题的dp方程实际上是不需要第一维的(也就是那个 \(i\)),所以我们设计以下状态:
bool dp[j][k] 表示 前 $i$ 个数是否可以构成和为 $j$,lcm 为 $k$ 的方案数。
然后来设计dp方程。我们在考虑dp方程最常用的办法是:先假设我们已经知道了前面的状态,然后考虑下一步的状态如何从这一步转移过来,这个是就顺推法。
这个公式应该比较好理解,就是新选一个数的影响会对哪些部分有影响。
那么如何来统计最后的有多少种不同的 lcm 呢?
一个比较朴素的想法是,枚举 dp[j][k]
,如果为 1 则说明这个方案合法,仅需统计不同的 k 即可。
然而有一个问题是,虽然这道题的数据范围 \(n,m\) 只有120,但是可能存在不超过此范围的 \(j\),对应了很大的 lcm 值。(这个值很可能爆 long long
)
比如:2 * 3 * 5 * 7 * 11 * 13 * 17 \(\cdots\)
由于不同的方案的同一个和对应了很多不同的 \(lcm\),所以我们可以考虑一个数字 lcm[j][k]
表示 和为 \(j\) 的方案所对应的第 \(k\) 个 \(lcm\) 是多少。用 \(cnt_j\) 表示和为 \(j\) 的 \(lcm\) 的个数。
用 vis[lcm]
表示 这个 \(lcm\) 是否选过,当 vis[lcm]
由 0 变 1 时,说明有一个新的 \(lcm\) 选入答案,所以 \(ans\)++.
新的问题产生了,怎么维护 lcm[j][k]
?
每选入一个新的 \(a_i\),会产生一个对应的新的 \(lcm\) ,问题在于什么时候将这个新的值存入 \(lcm[j][k]\)? 是不加分辨的直接存入,还是有条件的存入呢?
我们来回忆一下 \(dp[j][k]\) 的状态,表示是否可以用前 \(i\) 个数选出和为 \(j\),lcm 为 \(k\),的方案。
如果当 \(dp[j][k]\) 更新为 1 时,可以说明什么?说明这个新产生的 \(lcm\) 可以产生贡献。是的,新产生的 \(lcm\) 可能在之前产生过了,我们当然可以用其他数据结构比如 set
来维护,但是其实这个问题是可以用原有的dp数组解决的。
具体的公式为:
if ( dp[ j + a[i] ][ new_lcm ] == 0) {
dp[ j + a[i] ][ new_lcm ] = 1;
lcm[ j + a[i] ][ ++cnt[ j + a[i] ] ] = new_tmp;
}
最后,我们来考虑边界条件:
首先,选 0 个数也是一种方案,所以 dp[0][1] = 1
,对应的,lcm[0][1] = 1
,cnt[0] = 1
;
其次,由于选 0 个数时有 1 个 \(lcm\) ,那么 ans = 1
, vis[1] = 1
目标:ans
.
\(Code:\)
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int n, m, cnt[125], lcm[125][50000], ans, a[125];
// 虽然数据范围只有120,但前面提到过,其对应的lcm个数可能很多,经过计算最终的lcm个数不会超过50000
map <ll, bool> dp[125], vis;
// 同理,由于dp数组的第二维可能很大,于是用一个map做一个映射
// 代码里使用的是教练比较推荐到用map开二维数组的方法
void inp() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; ++i) scanf("%d", &a[i]);
return;
}
void work() {
// <------初始化------>
ll tmp = 0;
dp[0][1] = 1, lcm[0][1] = 1, cnt[0] = 1;
ans = 1, vis[1] = 1;
// <------DP更新------>
for(int i = 1; i <= n; ++i)
for(int j = 0; j <= m; ++j) // 注意从0开始,理同摆花
for(int k = 1; k <= cnt[j]; ++k) {
if(j + a[i] <= m) {
tmp = a[i] * lcm[j][k] / __gcd(a[i], lcm[j][k]);
// 注意搞清楚ans的依赖、lcm[j][k]的依赖
// 他们应该在什么时候更新?
if(vis[tmp] == 0) {
vis[tmp] = 1;
ans++;
}
if(dp[j+a[i]][tmp] == 0) {
dp[j+a[i]][tmp] = 1;
lcm[j+a[i]][++cnt[j+a[i]]] = tmp;
}
}
}
printf("%d\n", ans);
}
int main() {
inp();
work();
return 0;
}
然而,这样一份代码在第10个数据点跑出了惊人的 4.6 s。
我们来考虑这样一个问题(sum是这个方案的各权值的和),对于同一个 \(lcm\),它可能是来自于许多不同的sum,我们应该选这些sum里最小的那个,这样不会使最终的答案更差。
我们再开一个数组 qwq[lcm]
来记录这个当前的lcm所对应的和,显然先出现的和一定比后出现的和小。
同时用 map
的成员函数 count()
是比 直接用下标访问元素快的。
\(Code:\)
for(int i = 1; i <= n; ++i) {
qwq.clear();
for(int j = 0; j <= m; ++j) {
if(j + a[i] <= m){
for(int k = 1; k <= cnt[j]; ++k) {
if(qwq.count(lcm[j][k])) continue;
qwq[lcm[j][k]] = 1;
tmp = a[i] * lcm[j][k] / __gcd(a[i], lcm[j][k]);
if(!vis.count(tmp)) {
vis[tmp] = 1;
ans++;
}
if(!dp[j+a[i]].count(tmp)) {
dp[j+a[i]][tmp] = 1;
lcm[j+a[i]][++cnt[j+a[i]]] = tmp; //
}
}
}
}
}
这样优化出来的速度是 0.6 s。。。
完结 ~ 撒花 ~~~~