【解题报告】Educational Codeforces Round 20

来源:互联网 发布:java中的double类型 编辑:程序博客网 时间:2024/05/16 07:00

题目链接


A. Maximal Binary Matrix(Codeforces 803A)

思路

本题的入手点是,先满足“字典序最小”的要求,再满足对称性(就比较容易了)。
由于题目要求解的“字典序”最小,所以可以先按照行从上到下,再按照列从左到右的顺序为矩形填上 1 (也就是在作文纸上写字的顺序)。然后就只需要满足对称性了。假设当前我们已经填到 (i,j) 这个格子了,那么分以下两种情况

  1. i=j 时,由于当前填的位置位于对角线上,填上后肯定不会影响对称性,所以能填就填。什么是不能填的情况呢?如果填 1 的次数没有剩余了的话就不能填了。
  2. i!=j 时,由于当前填的位置不位于对角线上,所以如果要在 (i,j) 网格内填数的话就得在 (j,i) 网格内填数。有如下情况就不能填:剩余的填 1 次数为 1(i,j) 网格内已经填过 1 了。

代码

#include <bits/stdc++.h>using namespace std;const int maxn = 110;int n, k, a[maxn][maxn];// 输出矩阵void output() {    for(int i = 1; i <= n ;i++) {        for(int j = 1; j <= n; j++) {            cout << a[i][j] << ' ';        }        cout << endl;    }}int main() {    ios::sync_with_stdio(false);    cin.tie(0);    cin >> n >> k;    // k比格子还要多的时候    if(k > n * n) {        cout << -1 << endl;        return 0;    }    for(int i = 1; i <= n; i++) {        for(int j = 1; j <= n; j++) {            if(k == 0) {                output();                return 0;            }            if(a[i][j] == 1) {                continue;            }            // 填对角线位置            if(i == j) {                a[i][j] = 1;                k--;            }            // 填非对角线位置            else if(k >= 2) {                a[i][j] = a[j][i] = 1;                k -= 2;            }        }    }    if(k > 0) {        cout << -1 << endl;    }    else {        output();    }    return 0;}

B. Distances to Zero(Codeforces 803B)

思路

本题的入手点在于将问题“找离某位置最近的 0 ”分解成子问题“找在其左侧的最近的 0 ”和子问题“找在其右侧的最近的 0 ”。
我们只考虑“找在数组中的位置的 i 左侧最近 0 ”(因为“右侧”的情况雷同)。从 1n 遍历数组,同时维护一个变量 last ,其值表示在当前状态之前,遇见的最后一个 0 的位置,那么对于任意位置 iilast 就是该位置到左侧最近0的距离。

代码

#include <bits/stdc++.h>using namespace std;const int maxn = 2e5 + 10, INF = 3e5;int n, last, a[maxn], b[maxn], c[maxn];int main() {    ios::sync_with_stdio(false);    cin.tie(0);    cin >> n;    for(int i = 0; i < n; i++) {        cin >> a[i];    }    // 从左到右扫描数组,寻找离位置i最近的左侧的0    last = -1;    for(int i = 0; i < n; i++) {        if(a[i] == 0) {            b[i] = 0;            last = i;        }        else if(last >= 0) {            b[i] = i - last;        }        else {            b[i] = INF;        }    }    // 从右到左扫描数组,寻找离位置i最近的右侧的0    last = -1;    for(int i = n - 1; i >= 0; i--) {        if(a[i] == 0) {            c[i] = 0;            last = i;        }        else if(last >= 0) {            c[i] = last - i;        }        else {            c[i] = INF;        }    }    // 输出答案    for(int i = 0; i < n; i++) {        cout << min(b[i], c[i]) << ' ';    }    return 0;}

C. Maximal GCD(Codefoeces 803C)

思路

本题的入手点为考虑到数列的 gcd 必然是 n 的约数。
这题要构造一个数列,其需要满足两个条件,一个是个数为 k ,另一个是和为 n 。我们先考虑第二个条件。第二个条件首先告诉我们的是,数列的 gcd 必然是 n 的约数。那么我们可以先枚举 n 的约数以缩小搜寻的范围。假设当前枚举到的约数为 d ,那么我们要构造出来的序列必然是以 d 为首项, d 为公差的等差序列的子序列。那么我们可以做出猜想:如果该数据有解,我们就可以构造出这样一组解

d, 2d, 3d, ..., (k1)d, nsum(d,k1)

其中 sum(a,b) 表示以 a 为首项, b 为项数的数列的前 b 项和。然后可以证明“如果该组数据有解的话,那么上述构造方式构造出的一定是一组解”,还可以证明“如果上述方式构造不出解,那么该组数据一定无解”。因为我也想不到该怎么证明,所以这里就不叙述怎么证明了(不负责任的解题报告写手系列)。
最后就是实现的时候,由于输入的数据太大,需要注意溢出的问题。

代码

#include <bits/stdc++.h>using namespace std;typedef unsigned long long ll;vector <ll> divisor;ll n, k, ub, a, s, S;int main() {    ios::sync_with_stdio(false);    cin.tie(0);    cin >> n >> k;    // 按照以1首项1为公差的数列和求出k的上界    // 以避免下面出现的惩罚溢出    ub = 2 * (ll)(sqrt(1.0 * n) + 0.5);    if(k > ub) {        cout << -1 << endl;        return 0;    }    // 预处理出n的约数    for(ll i = 1; i * i <= n; i++) {        if(n % i > 0) {            continue;        }        divisor.push_back(i);        if(i != n / i) {            divisor.push_back(n / i);        }    }    // 对n的约数d    for(ll d : divisor) {        // 避免溢出,提前判断        if(k * (k - 1) / 2 >= n / d) {            continue;        }        // 求出数列的和        s = k * (k - 1) * d / 2;        if(n - s <= (k - 1) * d) {            continue;        }        // 更新最大公约数最小的解        if(d > a) {            a = d;            S = s;        }        a = max(a, d);    }    if(a == 0) {        cout << -1 << endl;        return 0;    }    // 输出数列    for(ll i = 1; i < k; i++) {        cout << a + (i - 1) * a << ' ';    }    cout << n - S << endl;    return 0;}

D. Magazine Ad(Codeforces 803D)

思路

本题的入手点是,利用数据的“二分性”,将求最值的问题转化为验证某个值是否可行。
我们可以枚举最长的行的长度 x ,然后验证在所有行的长度不超过 x 的情况下,是否能构造出不超过 k 行的文本。这样的最小的 x 就是答案。如何验证是否能构造呢?不妨将被’-‘和空格分割出的字符串称作“单词”。先考虑第一行,将单词一个一个加到第一行的末尾,直到第一行的文本的长度即将超过 x (也就是再加入单词该行的长度就会超过 x 的时刻)。然后再考虑第二行,第三行,……,直到将所有单词都处理完。这样构造的文本必然具有所有行长度不超过 x 情况下的最短行数(证明略)。
但是由于 x (最长的行的长度)的可能长度达到了 105 ,所以我们利用 x 的二分性(较小的 x 不能满足“最多只有 k 行”的条件,而较大的 x 可以满足)将枚举改成二分查找就行了,查找满足条件的最小的 x 即可。

代码

#include <bits/stdc++.h>using namespace std;string s;vector <int> vec;int k, last, l, r;// 判断对给定的最长行的长度是否有解bool ok(int x) {    int cnt = 0, sum = 0;    // 对于每个“单词”    for(int i = 0; i < vec.size(); i++) {        if(vec[i] > x) {            return false;        }        // 把该单词算在下行        if(sum + vec[i] > x) {            sum = vec[i];            cnt++;        }        // 把该单词算在该行        else {            sum += vec[i];        }    }    if(sum > 0) {        cnt++;    }    return cnt <= k;}int main() {    ios::sync_with_stdio(false);    cin.tie(0);    cin >> k;    getline(cin, s);    getline(cin, s);    last = 0;    // 预处理处单词    for(int i = 1; i < s.size(); i++) {        if(s[i] == ' ' || s[i] == '-') {            vec.push_back(i - last + 1);            last = i + 1;        }    }    // 有可能还有最后一个单词    if(last <= s.size() - 1) {        vec.push_back(s.size() - 1 - last + 1);    }    // 二分查找    l = -1;    r = s.size();    while(r - l > 1) {        int mid = (l + r) / 2;        if(ok(mid) == true) {            r = mid;        }        else {            l = mid;        }    }    cout << r << endl;    return 0;}

E. Roma and Poker(Codeforces 803E)

思路

本题的入手点是,由“多阶段决策”这个特征想到用 解决。
在不考虑数据范围的情况下显然可以用搜索来解决,但是本题的数据范围显然不允许我们这样做。因此需要另辟蹊径。不妨令状态 i 表示当前处理到输入的字符串的第 i 位,那么在状态 i 我们可以根据 s[i] 的值有不同的可能(有点类似于决策)。

s[i] 可能发生的情况 L 此时只能是输掉第i局 W 此时只能是赢下第i局 D 此时可以打平第i局 ? 此时可以输,可以赢,也可以打平第i局

虽然每次有这么多种状态转移情况,但是在每个状态 i ,状态转移的结果都是有限的。于是我们改写状态为 (i,j) ,表示当前在第 i 局,主人公的净胜分为 j 。那么可以用 d[i][j] 表示 (i,j) 这个状态是否可达(可达 d[i][j]=1 , 否则为 0 )。状态转移方程如下:

s[i] 状态转移方程 L d[i][j] |= d[i-1][j + 1] W d[i][j] |= d[i - 1][j - 1] D d[i][j] |= d[i - 1][j] ? (此处应写上以上的三条方程)

最后如果状态 (n,k)(n,k) 上的状态权值为 1 的话就表明有解。在转移过程中有两个需要注意的点:

  1. j 有可能是负数。解决方案是将所有的j都加上一个偏移量,将原来 j 可能落在的区间 [k,k] 映射到 [lb,ub] 上去( lb0,ub0 )。
  2. 由于在比赛的中途比赛不能终止(也就是一定要恰好在第 n 局结束),所以对于任意 i<n ,在状态转移中,任意状态不能转移到状态 (i,lb)(i,ub)

转移结束后还没有完成最终任务,我们还需要输出一个解。为了构造出一个解,我们可以用矩阵 p[i][j] 记录转移的过程,若从 (i1,k)(i,j) 有一个状态转移的话就令 p[i][j]=k 。最后就可以从 (n,lb)(n,ub) 根据 p 矩阵的值不断回溯来构造解了(当然,由于是从终状态开始回溯,所以构造出来的解是反过来的)。

代码

#include <bits/stdc++.h>using namespace std;const int maxn = 2010, mid = 1000;stack <char> ans;string s;int n, k, lb, ub, d[maxn][maxn], p[maxn][maxn];// 输的情况的状态转移void lose(int i, int j) {    if(j == ub || !d[i - 1][j + 1]) {        return;    }    d[i][j] = 1;    p[i][j] = j + 1;}// 赢的情况的状态转移void win(int i, int j) {    if(j == lb || !d[i - 1][j - 1]) {        return;    }    d[i][j] = 1;    p[i][j] = j - 1;}// 平局的情况的状态转移void draw(int i, int j) {    if(d[i - 1][j] == 0) {        return;    }    d[i][j] = 1;    p[i][j] = j;}int main() {    ios::sync_with_stdio(false);    cin.tie(0);    cin >> n >> k >> s;    ub = mid + k;    lb = mid - k;    s = " " + s;    d[0][mid] = 1;    for(int i = 1; i <= n; i++) {        for(int j = lb; j <= ub; j++) {            // 比赛不能提前结束            if(i < n && (j == ub || j == lb)) {                continue;            }            // 根据s[i]的值来决定状态的转移方式            if(s[i] == 'L') {                lose(i, j);            }            else if(s[i] == 'D') {                draw(i, j);            }            else if(s[i] == 'W') {                win(i, j);            }            else {                lose(i, j);                draw(i, j);                win(i, j);            }        }    }    // 无解的情况    if(!d[n][lb] && !d[n][ub]) {        cout << "NO" << endl;        return 0;    }    // 回溯构造解    int i = n;    int j = d[n][lb] ? lb : ub;    while(i > 0) {        if(p[i][j] > j) {            ans.push('L');        }        else if(p[i][j] == j) {            ans.push('D');        }        else {            ans.push('W');        }        j = p[i--][j];    }    // 逆序输出结果(这里用了一个栈)    while(!ans.empty()) {        cout << ans.top();        ans.pop();    }    return 0;}

F. Coprime Subsequences(Codeforces 803F)

思路

本题的入手点是,运用分类的思想,正难则反的思想和容斥的思想来解决计数问题。

在开始描述解法之前先做一些铺垫。令 cnt(a,f) 表示序列 a 中能被 f 整除的数的个数。 num(a,f) 表示序列 a 中满足性质“所有数都能被 f 整除”的子序列的个数(子序列指从原序列中拿出一些数构成的顺序不变的新序列)。显然,根据集合的计数原理也好,根据组合公式相加的公式也好,我们有

num(a,f)=2cnt(a,f)1

回到问题中。我们要求的是

a序列的满足性质“所有的数两两互质”的子序列的个数

“两两互质”听上去不那么清晰,可以将上述描述改成

a序列中满足性质“所有的数的最大公约数gcd为1”的子序列的个数

就算描述变成这样也还是不容易有思路,那么根据“正难则反”的思想,我们可以把上述描述进一步变形:

a序列中不满足性质“所有数的gcd为2或3或…或max{a_i, i <= n}”的子序列的个数

注意是“不满足”那个性质的子序列的个数。那么上述表述所描述的值就可以用容斥原理转化成求

num(a,1)num(a,2)num(a,3)num(a,5)...+num(a,2×3)+num(a,2×5)+num(a,3×5)...num(a,2×3×5)...

其中 cnt(a,f) 可以预处理出来, num(a,f) 可以通过快速幂算法求出来,上述式子中的 num(a,f) 的系数跟 f 有关,实际这个系数就是 μ(f) ,其中 μ() 就是著名的莫比乌斯函数(各大百科都能找到定义),用线性筛算法可以预处理出来。至于为什么系数是莫比乌斯函数,我觉得是以因为莫比乌斯函数正好对应了素因子容斥问题中的容斥原理公式的项的系数(如果不明白,那么请研究莫比乌斯函数,或者百度、谷歌其资料)。

代码

#include <bits/stdc++.h>using namespace std;typedef long long ll;const int maxn = 1e5 + 5, mod = 1e9 + 7;bool vis[maxn];int n, a, f[maxn], mu[maxn], prime[maxn];ll ans, cnt[maxn];// 筛法预处理出莫比乌斯函数// 存在mu[]中void init(int N) {    memset(vis, 0, sizeof(vis));    mu[1] = 1;    int cnt = 0;    for(int i = 2; i < N; i++) {        if(!vis[i]) {            prime[cnt++] = i;            mu[i] = -1;        }        for(int j = 0; j < cnt && i * prime[j] < N; j++) {            vis[i * prime[j]] = 1;            if(i % prime[j]) {                mu[i * prime[j]] = -mu[i];            }            else {                mu[i * prime[j]] = 0;                break;            }        }    }}// 枚举统计a数组中的各个数的约数的出现次数void divisor(int n) {    for(int i = 1; i * i <= n; i++) {        if(n % i == 0) {            cnt[i]++;            if(i != n / i) {                cnt[n / i]++;            }        }    }}// 计算快速幂的函数ll modPow(ll a, ll n, ll mm) {    ll ans = 1;    for(; n > 0; n >>= 1) {        if(n & 1) {            ans = (ans * a) % mm;        }        a = (a * a) % mm;    }    return ans;}int main() {    ios::sync_with_stdio(false);    cin.tie(0);    init(maxn);    cin >> n;    for(int i = 1; i <= n; i++) {        cin >> a;        divisor(a);    }    // 计算上面提到的num()函数    for(int i = 1; i < maxn; i++) {        f[i] = modPow(2, cnt[i], mod) - 1;        f[i] = (f[i] + mod) % mod;    }    // 容斥原理     for(int i = 1; i < maxn; i++) {        ans = (ans + mu[i] * f[i] + mod) % mod;    }    cout << ans << endl;    return 0;}

其它题目略

0 0
原创粉丝点击