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\) 的奇偶性分类讨论:

  1. 如果 \(a^2+1\) 是奇数,那么在蛇形矩阵(2)中的这一行是从左到右递增的;
  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方程最常用的办法是:先假设我们已经知道了前面的状态,然后考虑下一步的状态如何从这一步转移过来,这个是就顺推法。

\[dp[j][k] \Rightarrow dp[{j+a[i]][ \frac {k×a_i}{gcd(k, a_i)}} ] \]

这个公式应该比较好理解,就是新选一个数的影响会对哪些部分有影响。

那么如何来统计最后的有多少种不同的 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] = 1cnt[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。。。

完结 ~ 撒花 ~~~~

posted @ 2021-10-08 15:24  许江一墨  阅读(47)  评论(0编辑  收藏  举报