CF Round 697(Div3) 解题补题报告

官方题解链接

A题 Odd Divisor(数学/二进制)

给定 \(T(T \leq 10^4)\) 组数据。

判断一个数 \(n(2 \leq n \leq 10^{14})\) 是否存在一个大于 \(1\) 的奇数能够整除 \(n\),存在则输出"YES",反之输出 "NO"。

如果 \(n\) 是一个奇数,显然它自身可以整除自己(题目数据已经限制了,\(n\) 不可能为 \(1\),就不需要特判了),输出 "YES" 。

如果 \(n\) 是一个偶数,那么分两类:

  1. 它是 \(2\) 的幂,那么除了 \(1\),不可能存在其他的奇数因子,输出 "NO" 即可。

  2. 不是 \(2\) 的幂,那么这个数就可以变成 \(2^k*m\) 的形式(其中 \(m\) 是奇数),那么奇数 \(m\) 可以整除 \(n\),输出 "YES" 即可。

那么程序就很明显了:如果 \(n\)\(2\) 的幂就输出 "NO",反之输出 "YES"。

判断一个数 \(n\) 是不是 \(2\) 的幂很简单:一直除以 \(2\),直到无法被 \(2\) 整除,看剩下来这个数是不是 \(1\) 即可,复杂度 \(O(\log n)\)。不过,作为打 \(ACM\) 的我们,应当想出来一些更加简便快速的方法。

我们学树状数组的时候,需要寻找一个数的二进制里面的 \(1\) 的位置,当时接触过 \(lowbit\) 的概念:找到一个二进制里面最右边的 \(1\) 和它右边的 \(0\) 构成的数。显然,如果一个数 \(n\)\(2\) 的幂,那么 \(n = lowbit(n)\),我们依此进行判断即可。

另外,再介绍一个骚操作:\(n \& (n - 1)\),可以将一个数的二进制里面最右边的 \(1\) 变成 \(0\) (原理略),那么如果 \(n\)\(2\) 的幂,那么显然 \(n \& (n-1) = 0\)

贴出代码(只给出暴力的那种):

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int T;
    cin>>T;
    while (T--) {
        long long n;
        cin>>n;
        while (n % 2 == 0) n /= 2;
        if (n == 1) cout<<"NO"<<endl;
        else cout<<"YES"<<endl;
    }
    return 0;
}

B题 New Year's Number(数学)

给定 \(T(T \leq 10^4)\) 组数据。

判断一个数 \(n(1 \leq n \leq 10^6)\) 是否能表示为 \(2020a+2021b\) 的形式(\(a,b\geq 0\)),存在则输出"YES",反之输出 "NO"。

这题我们还可以转化一下:方程 \(2020x +2021y=n\) 有没有一组非负解?

我们给出结论:若存在\(k \geq 1\),使得 \(2020k \leq n \leq 2021k\),那么便存在这么一组解。

证明

方法1

\(2020(x+y) + y = n\),其中\(0 \leq y \leq x + y\)。不妨设 \(x+y=k\),那么就变成了\(2020k+b=n\),其中 \(0\leq b \leq k\)。那么我们得出结论:如果解存在,那么必然有 \(2020k \leq n \leq 2021k\),其中 \(k \geq 1\)。(似乎证的并不是很明)

方法2

对于方程 \(2020x+2021y=n\), 我们显然容易想到扩展欧几里得(exgcd)。

因为 \(\gcd(2020,2021)=1\),那么方程 \(2020x+2021y=1\) 有解,我们可以轻易找出一组:\(\begin{cases}x=-1\\y=1\end{cases}\)

同乘上 \(n\),便得到了 \(2020x+2021y=n\) 的一组解:\(\begin{cases}x=-n\\y=n\end{cases}\)

我们对这组解尝试进行变换,看看能不能给他们变成一组非负解。

换言之,我们需要找到一个正整数 \(k\),使得\(\begin{cases}x=2021k-n\\y=n - 2020k\end{cases}\) ,且 \(x,y \geq 0\)

转换后,也就是我们需要证明的:\(2020k \leq n \leq 2021k\)

判定

方法1

\(n \leq 10^6\),这个数据规模小的一,不管是每次都暴力判断,或者先预处理后回答询问,复杂度都完全可以接受。

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int T;
    cin>>T;
    while (T--) {
        int n;
        cin>>n;
        bool flag = false;
        for (int k = 1; k <= 600; ++k)
            if (2020 * k <= n && n <= 2021 * k) {
                flag = true;
                break;
            }
        cout<<(flag ? "YES" : "NO")<<endl;
    }
    return 0;
}

方法2

我们将 \(n\) 变成 \(2020a+b\) 的形式,即 \(a=n/2020\)\(b=n\%2020\)

如果 \(n\) 符合要求,那么显然 \(a \geq 1\)\(0 \leq b \leq a\) 。也就是说,我们只需要比较 \(n/2020\)\(n\%2020\) 的大小即可。

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int T;
    cin>>T;
    while (T--) {
        int n;
        cin>>n;
        cout<<((n/2020 >= n%2020) ? "YES" : "NO")<<endl;
    }
    return 0;
}

C题 Ball in Berland(组合数学)

给定 \(T(T \leq 10^4)\) 组数据。

每个班级中有 \(a\) 个男生和 \(b\) 个女生。总计 \(k\) 组关系,每组关系表明某一个男生和某一个女生可以组队。现在圣诞节要到了,老师需要选出两对男生女生来组成两个队伍(显然,一个人不可能在两个队里面)。求出可能的方案数。

\(1 \leq a,b,k \leq 2*10^5\)

方法1:组合数学

如果我们不考虑冲突,任选两组关系的话,那么就一共有 \(C_{k}^{2}\) 种选法。

我们不妨从反面切入,看看到底有多少组冲突的选法,然后用总选法减去冲突选法,剩下来的就是合法的选法。

冲突的原因只有一个:两个关系里面包含了同一个人。

那么我们可以从枚举具体的人来入手,对于每个人,如果他/她存在于 \(n\) 个关系之中,那么对于这个人,他就可以产生 \(C_{n}^{2}\) 组冲突(用行话来说,这又叫做贡献)。对于这种枚举方法,显然可以做到不重不漏。

假设第 \(i\) 个人(女生编号设为\([a + 1,a+b]\))存在于 \(m[i]\) 组关系之中,那么总冲突数目为\(\displaystyle\sum_{i=1}^{a+b} C_{m[i]}^2\)

所以答案 \(ans = {C_{k}^{2}}-\displaystyle\sum_{i=1}^{a+b} C_{m[i]}^2\)

注意了,记得开 long long,不光是算 \(C_k^2\) 的时候要记得用,后面的那个 \(m\) 数组也要开(记住这一点,我就是因为这玩意 \(WA\) 了不知道多少发)。干脆点的话,最好专门写一个算组合数的函数,不容易出错。

整体复杂度:\(O(Tk)\)(实际上由于题目表明总数据量有限制,实际上的复杂度仅有\(O(k)\))(我们默认下 \(k \geq a,b\)

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int a, b, k, m[N], n[N];
long long C2(long long x) {
    if (x < 2) return 0;
    return x * (x - 1) / 2;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) {
        int a, b, k, tmp;
        scanf("%d%d%d", &a, &b, &k);
        memset(m, 0, sizeof(int) * (a + 1));
        memset(n, 0, sizeof(int) * (b + 1));
        for (int i = 1; i <= k; ++i) {
            scanf("%d", &tmp); ++m[tmp];
        }
        for (int i = 1; i <= k; ++i) {
            scanf("%d", &tmp); ++n[tmp];
        }
        long long ans = C2(k);
        for (int i = 1; i <= a; ++i) ans -= C2(m[i]);
        for (int i = 1; i <= b; ++i) ans -= C2(n[i]);
        printf("%lld\n", ans);
    }
    return 0;
}

D题 Cleaning the Phone(前缀和,二分)

给定 \(T(T \leq 10^4)\) 组数据。

手机上有 \(n(n \leq 2*10^5)\) 个应用,第 \(i\) 个应用的占用内存为 \(a_i\),重要程度为 \(b_i\)\(b_i\) 值仅为 \(1\)\(2\))。现在我们需要删除一些应用,希望在释放至少 \(m(m \leq 10^9)\) 大小的内存的情况下,使得这些被删除应用的重要程度之和最小。如果无论怎么删都没法释放期望大小的空间,输出 \(-1\) 即可。

显然,我们可以将应用按照重要程度分成两类,对于每一类里面的应用,肯定是尽量删掉那些消耗内存比较多的,我们将他们从大到小排个序。

贪心吗?看数据规模好像确实可以,但是对于几种容易想出来的贪心策略,我们都可以构造出一些情况卡过去,pass。

背包?有点像,但是这个数据规模有亿点大了,没法跑过去,pass。

好家伙,那就只能枚举了。反正我们已经排好序了,直接从前到后枚举就行了。我们可以尝试着先构造出一个前缀和出来, \(s1[i]\) 表示前 \(i\) 个一类应用的占用内存之和, \(s2[i]\) 表示前 \(i\) 个二类应用占用内存之和。那么我们只需要找出 \(x,y\) ,使得在 \(s1[x]+s2[y] \geq m\) 的前提下令 \(x+2*y\) 最小。

如果直接暴力枚举的话,复杂度是 \(O(n^2)\) ,显然会 T 飞。我们考虑下咋优化吧。

很显然,这个 \(s\) 数组是单调递增的,如果 \(s1[x]+s2[y] \geq m\),显然对于 \(k \geq y\),都有\(s1[x]+s2[k] \geq m\)。既然如此,答案具有单调性质,那我们可以找出二分策略:枚举 \(x\),然后二分 \(y\),使得 \(s2[y]\geq m-s1[x]\)即可。这样的话,我们便可以将答案复杂度压到了 \(O(n\log n)\),达到题目要求,成功 \(AC\)

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, m, a[N], cnt1, cnt2, arr1[N], arr2[N];
//前缀和一定要记得开long long!(十年OI一场空,不开long long见祖宗)
long long s1[N], s2[N];
int solve()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    cnt1 = cnt2 = 0;
    for (int i = 1; i <= n; ++i){
        int tmp;
        scanf("%d", &tmp);
        if (tmp == 1) arr1[++cnt1] = a[i];
        else arr2[++cnt2] = a[i];
    }
    //排序
    sort(arr1 + 1, arr1 + cnt1 + 1, greater<int>());
    sort(arr2 + 1, arr2 + cnt2 + 1, greater<int>());
    //前缀和
    for (int i = 1; i <= cnt1; ++i)
        s1[i] = s1[i - 1] + arr1[i];
    for (int i = 1; i <= cnt2; ++i)
        s2[i] = s2[i - 1] + arr2[i];
    if (s1[cnt1] + s2[cnt2] < m) return -1;
    int ans = 1e9 + 10;
    for (int x = 0; x <= cnt1; ++x) {
        //一组特判,保证我的二分或着lower_bound肯定能找到解
        if (s1[x] + s2[cnt2] < m) continue;
        //我自己写的二分,但是STL里面有更方便的lower_bound,就不重复造轮子了
        /*
        int l = 0, r = cnt2 + 1, y;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (s2[mid] >= m - s1[x]) r = mid;
            else l = mid + 1;
        }
        y = l;
        */
        //注意,y可以为0,所以lower_bound的左端点是s2而不是s2+1(还好样例就能测出这个bug)
        int y = lower_bound(s2, s2 + cnt2 + 1, m - s1[x]) - s2;
        ans = min(ans, x + 2 * y);
    }
    return ans;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
        printf("%d\n", solve());
    return 0;
}

E题 Advertising Agency(贪心,组合数学)

给定 \(T(T \leq 10^4)\) 组数据。

现有 \(n(n \leq 1000)\) 个正整数,我们要求从中选取 \(k\) 个数,要使得其总和尽量大。问一共有几种选法。

答案可能过大,所以要求对 \(10^9+7\) 取模。

总和尽量大,显然我们应该贪心的选择,将他们排个序,尽量选那些大的。

如果这些数字各不相同,那么答案方案数显然只是 \(1\),所以我们思考,什么时候会出现多种方案的情况呢?

观察一组样例:\(n=4.k=3.a=[3,2,1,1]\),最大值显然为6,有两组方案。

我们可以发现不同方案的来源,对于一些数字(也就是排在入选方案里面最末尾的那些数字),因为他们的数量过多,导致了我们可以从中任选一些,而这任选的方案数也就是答案。

那么我们也就可以得到解题的两个步骤了:

  1. 确定可选数目 \(a\) 和需要选择的数目 \(b\)

  2. 算出组合数 \(C_a^b\)\(10^9+7\) 的值。

组合数我觉得用递推来预处理一下比较适合(主要是我也不会写分数取模)。

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
//C
const long long mod = 1e9 + 7;
long long C[N][N];

//
int n, k, a[N];
long long solve()
{
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    sort(a + 1, a + n + 1, greater<int>());
    int l = k, r = k;
    //注意,这里一定要加上边界的判断
    //我之前也觉得没有必要,因为a[1]=a[n+1]=0
    //但是这是多组数据,我并没有每次memset,所以a数组里面会有上一次的残留
    //例如上一次数组a 里面是 3 2 1 1 1,这一次里面是 3 2 1 1
    //我们以为 a[5]=0,实际上a[5]=1,这时候就会导致WA
    //所以对于多组数据,我们一定要严格管控,防止上一组数据的残留对下一组造成影响
    while (a[l - 1] == a[k] && l > 1) --l;
    while (a[r + 1] == a[k] && r < n) ++r;
    return C[r - l + 1][k - l + 1];
}
int main()
{
    for (int i = 1; i <= 1000; ++i)
        for (int j = 0; j <= i; ++j)
            if (j == 0 || j == i) C[i][j] = 1;
            else C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod;
    int T;
    scanf("%d", &T);
    while (T--)
        printf("%lld\n", solve());
    return 0;
}

F题 Unusual Matrix(数学,枚举)

给定 \(T(T \leq 1000)\) 组数据。

\(2\)\(n*n\) 大小的01矩阵(\(n \leq 1000\)),分别记作 \(A\)\(B\)

我们有两种操作:

  1. 选择矩阵的某一行,对该行的每一个元素和 \(1\) 进行异或(将这一行所有元素全部反转)

  2. 选择矩阵的某一列,对该列的每一个元素和 \(1\) 进行异或(将这一列所有元素全部反转)

判断矩阵 \(A\) 能否经过多次操作变为 \(B\)

我们可以对 \(A\)\(B\) 进行运算,构成新矩阵 \(C\),相同位置为\(0\),不同位置为 \(1\)。那么就变化为:能否将矩阵 \(C\) 经过多次操作全部置为 \(0\)

另外注意到,同一操作进行两次相当于没有操作过,这就意味着,对于某一格而言,操作在其上面的行列操作,各不会超过一次。

枚举吗?一共 \(n\)\(n\) 列,要枚举的话,每组数据的复杂度都是 \(O(2^{2n}+n^2)\),直接T飞,不谈。

数学知识?我想着镜像翻转,奇偶性判断啥的,一段时间也没有想出来啥思路,就在那一直僵着。

苦思冥想,我突然在枚举这条线上有了一个大突破:我们已知 \(A[1][1]\) 的值,如果是 \(0\),那么所在行和列有且只有一个会被反转。反之,要么都没有反转,要么都被反转了。

看起来似乎没啥用(我一开始也这么觉得的),但是如果我们细细挖下去,会发现一个细思恐极的现实:如果我们仅仅根据第一行和第一列各自有没有反转,外加 \(A\) 数组中第一行和第一列的数据,就足够构造出来所有行和列的反转情况了。在这种情况下,我们根据这些反转情况重新构造出一个新矩阵 \(D\),如果 \(C\)\(D\) 相等,那么这种构造方式就是合法的,也就是说,我们的矩阵 \(C\) 确实可以经过多次反转操作全部置为 \(0\)。反之,如果枚举的两种情况都无法成功,那就说明 \(C\) 无法通过这些操作全部置为 \(0\) 了。

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, A[N][N];
int dx[N], dy[N];
bool dfs(int state1, int state2) {
    dx[1] = state1, dy[1] = state2;
    for (int i = 2; i <= n; ++i)
        dx[i] = A[i][1] ^ dy[1];
    for (int i = 2; i <= n; ++i)
        dy[i] = A[1][i] ^ dx[1];
    for (int i = 1; i <= n; ++i)
        for (int j = 1; j <= n; ++j)
            if (A[i][j] != (dx[i] ^ dy[j])) return false;
    return true;
}
bool solve()
{
    scanf("%d", &n);
    char s[N];
    for (int i = 1; i <= n; ++i) {
        scanf("%s", s + 1);
        for (int j = 1; j <= n; ++j)
            A[i][j] = s[j] - '0';
    }
    for (int i = 1; i <= n; ++i) {
        scanf("%s", s + 1);
        for (int j = 1; j <= n; ++j)
            A[i][j] = (A[i][j] == s[j] - '0') ? 0 : 1;
    }
    bool ans;
    if (A[1][1] == 0) ans = dfs(0, 0) || dfs(1, 1);
    else              ans = dfs(1, 0) || dfs(0, 1);
    return ans;
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--)
        puts(solve() ? "YES" : "NO");
    return 0;
}

G题 Strange Beauty

给定 \(T(T \leq 10)\) 组数据。

\(n(n \leq 2*10^5)\) 个正整数,问至少去掉多少个数,可以让剩下来的所有数字中,两两都具有整除关系?

显然,我们最好给他们排一个序,这样就可以直接从小到大直接考虑了。

很显然的,我们可以构造出这样一个 \(DP\) 方程:记 \(dp[i]\) 为选取 \(a[i]\) 为整个剩下数列的最后一项时,这个剩下的序列的可能的最大长度。显然的,我们有转移关系:

\[dp[i]=\{\max\limits_{1\leq k <i,a[i]\%a[k]=0}dp[k]\}+1 \]

遗憾的是,线性递推的话,时间复杂度是 \(O(n^2)\) 的,会超时,所以我们必须想办法优化。

这里借鉴了程佬的思路,有兴趣的可以去他的博客看一下:链接

简言之,对于数列中的任意一项,他的最大值的规模和 \(n\) 相当,都是 \(2*10^5\)。我们注意到,枚举一个数的所有因子,其复杂度为 \(O(\sqrt{n})\)。我们只需要一开始再用一个数组,记录下某个数是否出现在数组里面,如果出现了,下标是多少,那么我们对于每一个数,就可以在 \(O(\sqrt{n})\) 的复杂度内找出其所有因子,然后判断是否在数组中,并且尝试更新 \(DP\) 数组。这样的话,我们就可以将每组数据的时间复杂度压进 \(O(n\sqrt{n})\),完全可以 \(AC\)

另外,这里有几个小注意点:

  1. 有可能会有重复数字。处理办法多种多样,最好的方法就是去重,然后再找一个数组记录一下这个数出现了几遍。
  2. 枚举因数的时候,记得不要把自己包含进去。

下面给出代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N], tmp[N], p[N], dp[N], vis[N];
void solve()
{
    //读入
    scanf("%d", &n);
    int cnt = 0;
    memset(p, 0, sizeof(p));
    for (int i = 1; i <= n; ++i)
        scanf("%d", &tmp[i]);
    //排序
    sort(tmp + 1, tmp + n + 1);
    //去重(一定要先排序再去重啊!!!)
    for (int i = 1; i <= n; ++i) {
        if (tmp[i] != a[cnt]) {
            //新元素
            a[++cnt] = tmp[i];
            p[cnt]=1;
        }
        else ++p[cnt];
    }
    swap(cnt, n);
    //下标
    memset(vis, 0, sizeof(vis));
    for (int i = 1; i <= n; ++i)
        vis[a[i]] = i;
    //DP
    //我这里特判了一下第一个元素,主要是为了防止1,其实也可以在下面加上特判
    dp[1] = p[1];
    for (int i = 2; i <= n; ++i) {
        dp[i] = p[i];
        //枚举因子
        for (int x = 1; x * x <= a[i]; ++x) {
            if (a[i] % x == 0) {
                int y = a[i] / x;
                if (vis[x]) dp[i] = max(dp[i], dp[vis[x]] + p[i]);
                if (y != a[i])
                    if (vis[y]) dp[i] = max(dp[i], dp[vis[y]] + p[i]);
            }
        }
    }
    int ans = -1;
    for (int i = 1; i <= n; ++i)
        ans = max(ans, dp[i]);
    printf("%d\n", cnt - ans);
}
int main()
{
    int T;
    scanf("%d", &T);
    while (T--) solve();
    return 0;
}
posted @ 2021-01-27 19:25  cyhforlight  阅读(152)  评论(0编辑  收藏  举报