Rank 663/3064. AC 7/12.

A. 阿宁的签到题

题意

根据分数输出等级。

思路

一门编程语言的基础之基础。

时间复杂度:\(O(1)\)

对应AC代码

import java.io.*;
import java.math.*;
import java.util.*;
import java.util.concurrent.atomic.*;

public class Main{

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        long x = scanner.nextLong();
        if(1 <= x && x <= 7) System.out.println("very easy");
        else if(x <= 233) System.out.println("easy");
        else if(x <= 10032) System.out.println("medium");
        else if(x <= 114514) System.out.println("hard");
        else if(x <= 1919810) System.out.println("very hard");
        else System.out.println("can not imagine");
    }

}

送分还不写快点啊((

B. 阿宁的倍数

题意

给定一个长度为 \(n\) 的数组 \(a\),下标从 \(1\) 开始,对于 \(q\) 次操作,输出需要输出的内容。

操作分为两种:

  1. 修改操作:数组末尾增加一个数 \(x\)
  2. 询问操作:对于所有 \(i>x\),输出有多少 \(a_i\)\(a_x\) 的倍数。

思路

我们考虑维护两个数组 \(tot,pre\)

其中,\(tot[a[i]]\) 表示整个序列有多少数是 \(a[i]\) 的倍数,\(pre[i]\) 表示 \([0, i]\) 区间内有多少数是 \(a[i]\) 的倍数。

那么,对于每次查询的 \(x\),输出 \(tot[a[x]]-pre[x]\) 即可。

我们来考虑一下这两个数组如何构建:

我们可以从前往后遍历,枚举 \(a[i]\) 的所有因数 \(j\),将所有 \(tot[j]\) 加上 \(1\),那么我们可以保证最后得到的 \(tot\) 是我们想要的数组,与此同时, 按照上述遍历方法,\(tot[a[i]]\) 就是 \(pre[i]\) 的值。

在修改操作时,我们只需在加入新加的数 \(x\) 的同时,更新 \(tot,pre\) 即可。

时间复杂度:\(O(n \sqrt n)\)

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int N = 400010;

int a[N], tot[N], pre[N];

signed main() {
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    int n, q;
    cin >> n >> q;
    for(int i=1;i<=n;i++){
        cin >> a[i];
        for(int j=1;j<=a[i]/j;j++){
            if(a[i] % j == 0){
                tot[j] ++;
                if(j != a[i] / j) tot[a[i] / j] ++;
            }
        }
        pre[i] = tot[a[i]];
    }
    while(q --){
        int op, x;
        cin >> op >> x;
        if(op == 1){
            a[++ n] = x;
            for(int j=1;j<=a[n]/j;j++){
                if(a[n] % j == 0){
                    tot[j] ++;
                    if(j != a[n] / j) tot[a[n] / j] ++;
                }
            }
            pre[n] = tot[a[n]];
        }else cout << tot[a[x]] - pre[x] << '\n';
    }
    return 0;
}

比较暴力但又不太暴力的做法

C. 阿宁的大背包

题意

给定背包的数量 \(n\)\(n\) 个背包的大小构成一个 \(n\) 的排列,按照将相邻背包合成一个新的大背包的方式,经过 \(n-1\) 次合并,得到一个大背包。输出一个排列,满足最终背包最大,并输出值。

合并方式:\([a, b, c, d] \rightarrow [a + b , b + c , c + d]\)

思路

我们直接来考虑 \(5\) 个物品时的合并结果:

\(\begin{array}{l}>>[a, b, c, d, e] \\ => [a + b, b + c, c + d, d + e] \\ => [a + 2b + c, b + 2c + d, c + 2d + e] \\ => [a + 3b + 3c + d, b + 3c + 3d + e] \\ => [a + 4b + 6c + 4d + e] \end{array}\)

我们不妨留意一下从第二次合并后每项的系数,没错,就是杨辉三角。

于是,构建数组就非常明显了:我们只要按 中间向两侧递减 排列即可。

接着,考虑到数据范围并不大,于是下列两种方法均可行:

  1. 暴力合并;
  2. 计算出杨辉三角,作为系数和数组相乘。

考虑到暴力合并更不用脑子,这边采取方案 \(1\)

时间复杂度:\(O(n ^ 2)\)

对应AC代码

import java.io.*;
import java.math.*;
import java.util.*;
import java.util.concurrent.atomic.*;

public class Main{

    public static void main(String[] args) throws Exception{
        Console console = new Console();
        int n = console.nextInt();
        long mod = 1000000007;
        int[] a = new int[n];
        for(int i=0;i<n/2;i++) a[i] = i * 2 + 1;
        if(n % 2 == 1) a[n / 2] = n;
        for(int i=0;i<n/2;i++) a[n - i - 1] = (i + 1) * 2;
        List<Integer> ans = new ArrayList<>();
        for(int i=0;i<n;i++) ans.add(a[i]);
        for(int t=n;t>=2;t--){
            List<Integer> now = new ArrayList<>();
            for(int i=0;i<t-1;i++) now.add((int)(((long) ans.get(i) + ans.get(i + 1)) % mod));
            ans = now;
        }
        console.println(ans.get(0));
        for(int i=0;i<n;i++) console.print(a[i] + " ");
        console.close();
    }

    //快读模板,此处略去
    //public static class Console implements Closeable {}

}

优雅的暴力

D. 阿宁的毒瘤题

题意

给定一个字符串 \(s\),修改任意一个字符为其他字符,让子序列 \(udu\) 的数量最小,子序列不一定连续。输出修改后的 \(s\)

思路

不是 \(dp\) !!!!

首先,如果不删掉字符的话,做法就是很简单的 \(dp\),但这题如果用 \(dp\) 解的话,会特别麻烦,且我无法证明正确性

反而,这题是一道偏模拟的前缀和。

我们分别考虑删掉一个 \(d\) 和删掉一个 \(u\) 的代价:

  1. 对于一个 \(d\),它的价值为 \(u_{pre} \times u_{suf}\)
  2. 对于一个 \(u\),它的价值为 \(ud_{pre} + du_{suf}\)

于是,我们可以考虑前缀和的方法,统计正方向第 \(i\) 位前面有多少 \(u\),反方向后面有多少 \(u\) 即可。

注意,不止有 \(u,d\) 这两个字符,不要偷懒不写 \(else\ if\)

时间复杂度:\(O(n)\)

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int N = 200010, inf = 0x3f3f3f3f;

int pre[N], ps[N], ss[N];
string s;

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> s;
    int n = s.length();
    int cu = 0;
    for(int i=0;i<n;i++) if(s[i] == 'u') cu ++;
    int maxx = 0, maxi = 0;
    for(int i=0;i<n;i++){
        if(i > 0) {
            pre[i] = pre[i - 1];
            ps[i] = ps[i - 1];
        }
        if(s[i] == 'u'){
            pre[i] ++;
        }else if(s[i] == 'd'){
            if(maxx < pre[i] * (cu - pre[i])){
                maxx = pre[i] * (cu - pre[i]);
                maxi = i;
            }
            ps[i] += pre[i];
        }
    }
    for(int i=n-1;i>=0;i--){
        ss[i] = ss[i + 1];
        if(s[i] == 'd') ss[i] += (cu - pre[i]);
        else if(s[i] == 'u'){
            if(maxx < ps[i] + ss[i]){
                maxx = ps[i] + ss[i];
                maxi = i;
            }
        }
    }
    for(int i=0;i<n;i++) cout << (maxi == i ? 'a' : s[i]);
}

做了一个小时dp,最后10分钟才大彻大悟,人快哭出来力

E. 阿宁的生成树

待补充

F. 阿宁的二进制

题意

给定一个长度为 \(n\) 的数组 \(a\),下标从 \(1\) 开始,定义 \(F(x)=cnt_1\ of\ binary\ x\)。如 \(F(5)=2\)

对于独立的 \(q\) 次询问,定义一次操作为选定任意一个 \(i\),执行 \(a_i=F(a_i)\)。给定操作数 \(k\) ,输出 整个数组的最大值 的最小值。

每次询问输出答案后,数组 \(a\) 恢复原样。

思路

我们不妨来考虑 \(1e9\) 范围内二进制下 \(1\) 最多的数,它总共有 \(30\)\(1\)

对于最大值 \(30\),我们可以知道,第二次操作后最多只会有 \(4\)\(1\)

继续操作,剩下最多 \(2\)\(1\)

继续操作,剩下 \(1\)\(1\)

也就是说,对于任意 \(1e9\) 范围内的数,我们最多也只能进行 \(4\) 次操作,之后值就为固定的 \(1\)

换句话说,题给 \(k\) 的范围是唬人的,真正 \(k\) 的范围应为 \(8 e 5\)

那么,我们不妨先将所有询问都读入,然后桶排序一下,再枚举操作 \(p\) 次后的答案,若次数和询问相同,那么记录答案。我们不妨用大根堆来存储,让时间复杂度降到 \(k \log k\)

时间复杂度:\(O(n \log n)\)

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int N = 200010;

pair<int, int> qs[N];
int ans[N];

int F(int x) {
    int cnt = 0;
    while (x != 0) {
        if ((x & 1) == 1) cnt++;
        x >>= 1;
    }
    return cnt;
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, q;
    cin >> n >> q;
    priority_queue<int> pq;
    for(int i=0;i<n;i++) {
        int t;
        cin >> t;
        pq.emplace(t);
    }
    for(int i=0;i<q;i++){
        int t;
        cin >> t;
        qs[i] = {t, i};
    }
    sort(qs, qs + q, [](pair<int, int> o1, pair<int, int> o2){return o1.first < o2.first;});
    int i = 0, to = 0;
    while(to < q){
        int t = pq.top();
        if(t == 1) break;
        pq.pop();
        i ++;
        pq.push(F(t));
        while(to < q && qs[to].first == i) ans[qs[to ++].second] = pq.top();
    }
    for(int t=0; t < q; t++) {
        if(ans[t] == 0) cout << 1 << '\n';
        else cout << ans[t] << '\n';
    }

}

自从上次做过某题后,老想着会不会可以收敛((

G. 阿宁的整数配对

题意

给定一个长度为 \(n\) 的数组 \(a\),选出 \(k\) 对整数,输出每对整数相乘并求和的最大值。

思路

排一个序,从两端取即可。

时间复杂度:\(O(n \log n)\)

对应AC代码

import java.io.*;
import java.math.*;
import java.util.*;
import java.util.concurrent.atomic.*;

public class Main{

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(), k = scanner.nextInt();
        long[] a = new long[n];
        for(int i=0;i<n;i++) a[i] = scanner.nextInt();
        Arrays.sort(a);
        int l = 0, r = n - 1;
        long ans = 0;
        for(int i=0;i<k;i++){
            if(a[l] * a[l + 1] > a[r] * a[r - 1]){
                ans += a[l] * a[l + 1];
                l += 2;
            }else{
                ans += a[r] * a[r - 1];
                r -= 2;
            }
        }
        System.out.println(ans);
    }

}

打卡打卡~

H. 阿宁讨伐虚空

题意

给定 \(x\) 个敌人,在 \([L,R]\) 内随机选一个 \(y\) ,若 \(y < x\),那么敌人能被攻击到。输出能被攻击到的概率。

思路

如题,分类讨论算一下概率即可。

时间复杂度:\(O(1)\)

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int N = 100010;

signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int x, l, r;
    cin >> x >> l >> r;
    if(l > x - 1) cout << 0; 
    else if(r < x) cout << 1;
    else cout << ((double) (x - l) / (double) (r - l + 1));
}

简简单单签到题

I. 阿宁前往沙城

题意

给定一个无向图,定义操作为选定两条边,将一条边删除,并将另一条边的长度改为 \(1\)。在 操作可在任意时间可执行无限次 的条件下,输出 \(1\)\(n\) 的最短路。

思路

很显然,从第二条路开始,我们直接把前面的路毁掉即可。

所以我们不妨直接把所有边改成 \(1\),用 \(dijkstra\) 跑一遍最短路即可。

但得到的答案会出现一种特殊情况:最短路将所有边都覆盖了。

在该情况下,第一条只能用原来的长度替代了。

因此,直接套板子然后略微修改一下即可。

时间复杂度:\(O(n \log n)\)

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int N = 200010;

struct edge {
    int v, w;
};

struct node {
    int dis, u;
    bool operator>(const node& a) const { return dis > a.dis; }
};

vector<edge> e[N];
int dis[N], vis[N], cnt[N];
priority_queue<node, vector<node>, greater<> > q;

void dijkstra(int s) {
    memset(dis, 0x3f, sizeof(dis));
    dis[s] = 0;
    q.push({0, s});
    while (!q.empty()) {
        int u = q.top().u;
        q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (auto ed : e[u]) {
            int v = ed.v, w = ed.w;
            if (dis[v] > dis[u] + w) {
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u] + 1;
                q.push({dis[v], v});
            }
        }
    }
}

signed main(){
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int n, m;
    cin >> n >> m;
    int minn = 0x3f3f3f3f;
    for(int i=0;i<m;i++){
        int u, v, w;
        cin >> u >> v >> w;
        if(u == 1 || v == 1) minn = min(minn, w);
        e[u].push_back({v, 1});
        e[v].push_back({u, 1});
    }
    dijkstra(1);
    cout << (cnt[n] < m ? cnt[n] : dis[n] + minn - 1);
}

oi-wiki的板子真好用(划掉

J. 阿宁指指点点

待补充

K. 阿宁大战小红

待补充

L. 阿宁睡大觉

题意

给定一个 \(n\)\(n-i+1\) 列的地图(正方形的左上角),每行的最后一个格子是美梦格子,除 \(m\) 个噩梦格子外,其余格子都可以通过,输出从 \((1,1)\) 走到美梦格子的方案总数。

注意,\(m \leq 10\)

思路

这题有一个很明显的特点:障碍数远小于总格子数。

不考虑噩梦格子的话,总方案数很好求,即为 \(2^{n-1}\)。但若用类似于 \(dfs\) 的方法去枚举能走的路径,显然是过于复杂的。

有没有一种算法,可以用类似于取补集的方法来大大降低时间复杂度呢?

也许我们可以枚举不能走的路径,但暴力枚举也是不行的。

这里需要用到 容斥

对于两个噩梦格子,它们之间的方案数可以用组合数来求:

每条路径的节点数量是一致的,为 \((\Delta x - 1) + (\Delta y - 1)\),而每条路径一定会有 \(\Delta x - 1\) 个节点是在 \(x\) 轴方向移动的,所以方案数即为 \(C_{\Delta x + \Delta y - 2}^{\Delta x - 1}\)

于是,我们只需枚举所有选择即可,这里我们可以考虑用二进制进行状态压缩,直接用二进制位是否是 \(1\) 来考虑这个噩梦节点是否选上。当然也可以无脑递归套 \(for\),但状压会更好写。

还有一个问题,若我们每次都算一遍两个节点的方案数,未免有些复杂,所以我们可以用 \(O1\) 复杂度的组合数求法

容斥

我们来考虑三个集合,它们两两相交,且有一部分三个相交在一起,如下图:

容斥原理 - venn 图示例

对,这是一个韦恩图,而且我们可以很容易的得到下面这个式子:

\(|A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |B \cap C| - |C \cap A| + |A \cap B \cap C|\)

推广之后,对于 \(n\) 个集合的并集,我们只需按上述式子写,其中符号取决于选了几个集合,奇数为正偶数为负。

因而,对于所有噩梦格子的走法,利用容斥即可解决。

线性复杂度的组合数求法

显然,当数据量过大的时候,每次都用一遍 \(for\) 循环是不合理的,那么我们可以考虑预处理阶乘。

由于存在除法取模,我们需要用到乘法逆元,将除法取模转化为乘法取模。

逆元有一个很简单的求法,即费马小定理:对于 \(ab \equiv 1 \pmod p\),乘法逆元 \(b = a ^ {p - 2}\)

但每次用一遍快速幂也会提高时间复杂度,因而我们考虑线性求逆元,用到如下递推式:

\(inv[i] = (mod - mod / i) \times inv[mod\%i] \ \%mod\)

对于阶乘的逆元,满足 \(facInv[i] = facInv[i - 1] \times inv[i]\ \% mod\)

满足上述条件后,\(C_n^m = fac[n] \times facInv[m]\ \% mod \times facInv[n - m]\ \% mod\)

线性求逆元的证明

img

时间复杂度:\(O(m \times 2 ^ m)\)

对应AC代码

#include <bits/stdc++.h>

using namespace std;

#define int long long
#define pii pair<int, int>

const int N = 400010, mod = 1e9 + 7;

pii a[N], b[N];
int n, m, inv[N], fac[N], facInv[N], pow2[N];

//O1复杂度求组合数
int c(int n, int m){
    return fac[n] * facInv[m] % mod * facInv[n - m] % mod;
}

signed main() {
    ios::sync_with_stdio(0);
    cin >> n >> m;
    for(int i=0;i<m;i++) cin >> a[i].first >> a[i].second;
    inv[1] = fac[0] = fac[1] = facInv[0] = facInv[1] = pow2[0] = 1;
    for(int i=2;i<=n*2;i++){
        inv[i] = (mod - mod / i) * inv[mod % i] % mod;
        fac[i] = fac[i - 1] * i % mod;
        facInv[i] = facInv[i - 1] * inv[i] % mod;
    }
    for(int i=1;i<=n;i++) pow2[i] = pow2[i - 1] * 2 % mod;
    int ans = pow2[n - 1];
    for(int i=1;i<(1 << m);i++){
        int t = 0;
        b[t ++] = {1, 1};
        int sign = 1;
        for(int j=0;j<m;j++){
            if((i >> j) & 1){
                b[t ++] = a[j];
                sign = -sign;
            }
        }
        sort(b, b + t);
        int now = pow2[n - 1 - (b[t - 1].first + b[t - 1].second - 2)];
        for(int j=1; j < t; j++){
            if(b[j - 1].second <= b[j].second){
                int x = b[j].first - b[j - 1].first + 1, y = b[j].second - b[j - 1].second + 1;
                now = now * c(x + y - 2, x - 1) % mod;
            }else{
                now = 0;
                break;
            }
        }
        ans += sign * now;
    }
    ans = (ans % mod + mod) % mod;
    cout << ans << '\n';
    return 0;
}

有点震撼的说...