[做题日记#1] Educational Codeforces Round #9

场次链接:Educational Codeforces Round 9


这场还挺有意思的?可能大多都是图论题和一些经典的DS,导致补的很舒服吧()


A.Grandma Laura and Apples


题目大意:


n n n 个人按顺序买苹果,每次只买你手上剩下的一半,如果不能整除则免费多送一个苹果(即减一再除二)。每个苹果 p p p 元,送出的苹果不算价钱。最后你手上苹果卖完了,问你最后卖了多少钱。

解题思路:


直接模拟即可,记得开 l o n g long long l o n g long long

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

AC代码:


void solve() {
    int n, p;
    cin >> n >> p;
    vector<string> str(n);
    for (auto& s : str) {
        cin >> s;
    }
 
    i64 last = 0, del = 0;
    while (str.size()) {
        last <<= 1;
        if (str.back().find("plus") != string::npos) {
            ++last;
            del += p;
        }
        str.pop_back();
    }
 
    cout << last * p - del / 2 << '\n';
}

B.Alice, Bob, Two Teams


题目大意:


n n n 个棋子站在一排,只会是 A A A B B B 两种,第 i i i 个位置上棋子的值为 a i a_i ai 。你可以选择前缀的一段区间或是后缀的一段区间,然后将 A A A 改成 B B B ,将 B B B 改成 A A A

问你最后为 B B B 的棋子的值之和最大为多少。

解题思路:


枚举一下前后缀的反转位置,取最优即可。

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

AC代码:


void solve() {
    int n;
    cin >> n;
 
    vector<int> arr(n);
    for (auto& v : arr) {
        cin >> v;
    }
 
    string str;
    cin >> str;
 
    i64 sumA = 0, sumB = 0;
    for (int i = 0; i < n; ++i) {
        if (str[i] == 'A') sumA += arr[i];
        else sumB += arr[i];
    }
 
    sumA = 0;
 
    i64 A = 0, B = 0, ans = sumB;
    for (int i = 0; i < n; ++i) {
        if (str[i] == 'A') A += arr[i];
        else B += arr[i];
        ans = max(ans, sumB - B + A);
    }
 
    A = B = 0;
    for (int i = n - 1; i >= 0; --i) {
        if (str[i] == 'A') A += arr[i];
        else B += arr[i];
        ans = max(ans, sumB - B + A);
    }
 
    cout << ans << '\n';
}

C.The Smallest String Concatenation


题目大意:


给出 n n n 个字符串,你要根据某个顺序来拼接字符串,使得拼接得到的最终字符串字典序最小。

解题思路:


考虑一个问题,假设我只有字符串 S S S T T T 。那么显然我只要查看 S + T S + T S+T T + S T + S T+S 哪个的字典序最小即可。

扩展一下这个情况,假设我们有一个字符串数组 S S S

假设有 ( S 0 + S 1 < S 1 + S 0 ) (S_0+S_1<S_1+S_0) (S0+S1<S1+S0) ( S 1 + S 2 < S 2 + S 1 ) (S_1+S_2<S_2+S_1) (S1+S2<S2+S1)

那么 S 0 + S 1 + S 2 S_0+S_1+S_2 S0+S1+S2 一定是一个最小的字典序。我们根据 S i + S j < S j + S i S_i+S_j<S_j+S_i Si+Sj<Sj+Si 对整个字符串排序即可。

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

AC代码:


void solve() {
    int n;
    cin >> n;
    vector<string> str(n);
    for (auto& s : str) {
        cin >> s;
    }
    sort(str.begin(), str.end(), [&](const string& s1, const string& s2) {
        return s1 + s2 < s2 + s1;
    });
    for (auto& s : str) {
        cout << s;
    }

D.Longest Subsequence


题目大意:


给出一个长为 n n n 的数组和一个数字 m m m ,你要选出一些数字 a i a_i ai 组成一个新数组使得 l c m ( c 1 , c 2 , . . . , c k ) ≤ m lcm(c_1,c_2,...,c_k) \leq m lcm(c1,c2,...,ck)m ,问你组成的数组长度 k k k 最大为多少(其中 l c m ( a , b ) lcm(a,b) lcm(a,b) 代表所有数字 a , b a,b a,b 的最大公倍数)。

规定空数组的 l c m lcm lcm 1 1 1

解题思路:


容易发现 a i ≤ 1 0 9 a_i \leq 10^{9} ai109 的条件是没用的,因为 m ≤ 1 0 6 m \leq 10^{6} m106,我们只需要考虑 a i ≤ 1 0 6 a_i \leq 10^{6} ai106 的数字。

假设我们固定了 l c m lcm lcm 的值,那么整个数组中我们只能选出 l c m lcm lcm 的因子。否则它们的 l c m lcm lcm 值一定会和我们当前固定的 l c m lcm lcm 不同。

但是枚举 l c m lcm lcm 再去找 a i a_i ai 显然复杂度不对,我们可以换个角度考虑,只用考虑 a i a_i ai 作为因子能对哪个 l c m lcm lcm 有贡献即可。

先把所有 a i a_i ai 装桶里,然后对于每个 a i a_i ai 的倍数(一定会对这个倍数有贡献)加上该数字的贡献(数量)即可,时间复杂度为调和级数级别的。

时间复杂度: O ( m log ⁡ m ) O(m \log m) O(mlogm)

AC代码:


void solve() {
    int n, m;
    cin >> n >> m;
    vector<int> arr(n + 1);
    vector<int> cnt(m + 1), ans(m + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> arr[i];
        if (arr[i] <= m) {
            ++cnt[arr[i]];
        }
    }
    for (int i = 1; i <= m; ++i) {
        for (int j = i; cnt[i] && j <= m; j += i) {
            ans[j] += cnt[i];
        }
    }
    int lcm = max<int>(1, max_element(ans.begin(), ans.end()) - ans.begin());
    cout << lcm << " " << ans[lcm] << '\n';
    for (int i = 1; i <= n; ++i) {
        if (lcm % arr[i] == 0) {
            cout << i << " ";
        }
    }
    cout << '\n';
}

E.Thief in a Shop


题目大意:


你有一个长度为 n n n 的数组 a a a ,和一个数字 k k k,每个 a i a_i ai 的数量是无限的。你可以拿任意多个 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an 构成新的数组 c c c ,但数组 c c c 的长度必须恰好为 k k k,问你有多少个不同的 ∑ i = 1 k c i \sum_{i=1}^{k} c_i i=1kci 能被凑成。

解题思路:


完全背包。

假设当前数组 a a a 已经排好了序,那么我们能够凑成的最小值一定是 k ⋅ a 1 k \cdot a_1 ka1,没有更小的方法了。

d p [ i ] dp[i] dp[i] 为凑成 k ⋅ a 1 + i k \cdot a_1+i ka1+i 所需要的非 a 1 a_1 a1 的数字的个数。

假设 d p [ i ] = 0 dp[i] = 0 dp[i]=0 那么就是选了 k k k a 1 a_1 a1 就能构成。
假设 d p [ i ] = 1 dp[i] = 1 dp[i]=1 那么就是选了 ( k − 1 ) (k-1) (k1) a 1 a_1 a1 然后再选一个 a j a_j aj 构成的。

那么我们只需要考虑把一些 a 1 a_1 a1 换成一些更大的数字即可。当然,我们可以把问题变得更简单,让所有 a 2 , a 3 , . . . , a n a_2,a_3,...,a_n a2,a3,...,an 都减去一个 a 1 a_1 a1,这样我们就只用考虑数字的增量。

接下来那么就是做一个完全背包就好了。

时间复杂度: O ( n k max ⁡ ( a i ) ) O(nk\max( a_i)) O(nkmax(ai))

AC代码:


void solve() {
    int n, k;
    cin >> n >> k;
    vector<int> arr(n);
    for (auto& v : arr) {
        cin >> v;
    }
    sort(arr.begin(), arr.end());
    int mn = arr[0], mx = arr.back();
    for (auto& v : arr) {
        v -= mn;
    }
 
    int m = mx * k + 5;
    vector<int> dp(m, 1e9);
    dp[0] = 0;
    mn *= k;
    for (int i = 1; i < n; ++i) {
        for (int j = arr[i]; j < m; ++j) {
            dp[j] = min(dp[j], dp[j - arr[i]] + 1);
        }
    }
    cout << '\n';
    for (int i = 0; i < m; ++i) {
        if (dp[i] <= k) {
            cout << mn + i << " ";
        }
    }
}

F.Magic Matrix


题目大意:


给出一个 n × n n \times n n×n 的矩阵 A A A

一个矩阵是魔法矩阵当且仅当:

  • 主对角线上的元素为 0 0 0 ,即 a i , i = 0 a_{i,i}=0 ai,i=0
  • 主对角线对称位置上的元素相等,即 a i , j = a j , i a_{i,j}=a_{j,i} ai,j=aj,i
  • 存在一个 k k k,使得 1 ≤ k ≤ n 1 \leq k \leq n 1kn a i , j ≤ max ⁡ ( a i , k , a j , k ) a_{i,j} \leq \max(a_{i,k},a_{j,k}) ai,jmax(ai,k,aj,k)

给出这个矩阵,你要判断当前矩阵是不是魔法矩阵。

解题思路:


b i t s e t bitset bitset 乱搞的做法,这里讲的是正解。

第一第二个条件很好判断,我们只考虑第三个情况。

对于这个条件 a i , j ≤ max ⁡ ( a i , k , a j , k ) a_{i,j} \leq \max(a_{i,k},a_{j,k}) ai,jmax(ai,k,aj,k) 我们仍然可以继续展开,我们发现 a j , k = a k , j ≤ max ⁡ ( a k , l , a l , j ) a_{j,k}=a_{k,j} \leq \max(a_{k,l},a_{l,j}) aj,k=ak,jmax(ak,l,al,j),那么带入就是 a i , j ≤ max ⁡ ( a i , k , a k , l , a l , j ) a_{i,j} \leq \max(a_{i,k},a_{k,l},a_{l,j}) ai,jmax(ai,k,ak,l,al,j) 同理,我们也可以继续展开,然后得到一长串东西取 max ⁡ \max max

这样很难看出东西,我们固然可以把矩阵元素 a i , j a_{i,j} ai,j 看成一条 ( i , j ) (i,j) (i,j) 的无向边,那么题意就转化成了:

对于所有的不包括边 ( i , j ) (i,j) (i,j) ( i → j ) (i \rightarrow j) (ij) 路径而言(路径的全集),边 ( i , j ) (i,j) (i,j) 的权值 ≤ \leq 路径边权的最大值。

例如 a i , j ≤ max ⁡ ( a i , k , a k , l , a l , j ) a_{i,j} \leq \max(a_{i,k},a_{k,l},a_{l,j}) ai,jmax(ai,k,ak,l,al,j)

我们可以看成 ( i → k → l → j ) (i \rightarrow k \rightarrow l \rightarrow j) (iklj) 的路径,且要满足边权 a i , j ≤ max ⁡ ( a i , k , a k , l , a l , j ) a_{i,j} \leq \max(a_{i,k},a_{k,l},a_{l,j}) ai,jmax(ai,k,ak,l,al,j)

注意到这是个经典的最小瓶颈路问题:

最小瓶颈路 问题是指在一张无向图中,询问点对 ( u , v ) (u,v) (u,v) 的一条简单路径,需要找出从 u u u v v v 的一条简单路径,使路径上所有边中最大值最小。其中无向图最小生成树中从 u u u v v v 的路径一定是 u u u v v v 的最小瓶颈路之一(因为最小生成树可能不唯一)。

那么问题就转化成了,只用考虑 ( i → j ) (i \rightarrow j) (ij) 路径上的瓶颈边即可。

可以用 k r u s k a l kruskal kruskal 重构树之类的方法来做,但其实我们可以在跑 k r u s k a l kruskal kruskal 的同时直接计算出来。

考虑 a i , j a_{i,j} ai,j 这条边是不是 ( i → j ) (i \rightarrow j) (ij) 的路径上的最大值。显然,所有小于等于 a i , j a_{i,j} ai,j 的边都无影响,我们跑最小生成树时直接合并严格小于 a i , j a_{i,j} ai,j 的边即可(先往下看)。

那什么时候我们才会比路径上的所有边都要大呢?

如果出现: ( i → j ) (i \rightarrow j) (ij) 只需要依靠比 a i , j a_{i,j} ai,j 权值严格小于的边,那么 a i , j a_{i,j} ai,j 一定大于这条最小瓶颈路上的所有边。

对所有边 ( i , j ) (i,j) (i,j) 判断一下 i , j i,j i,j 的可达性即可,注意不能同时合并所有权值相等的边(会影响并查集判断)。

当然,可以用 P r i m Prim Prim 将时间复杂度优化至 O ( n 2 ) O(n^2) O(n2),但个人认为 k r u s k a l kruskal kruskal 做法更容易理解一些。

时间复杂度: O ( n 2 log ⁡ n 2 ) O(n^{2} \log n^{2}) O(n2logn2)

AC代码:


struct DSU {
    vector<int> fa, siz;
    DSU(int n) : fa(n + 1), siz(n + 1, 1) { iota(fa.begin(), fa.end(), 0); };
    int find(int x) {
        while (x != fa[x]) x = fa[x] = fa[fa[x]];
        return x;
    }
    int size(int x) { return siz[find(x)]; }
    bool same(int x, int y) { return find(x) == find(y); }
    bool merge(int x, int y) {
        x = find(x), y = find(y);
        if (x == y) return false;
        siz[y] += siz[x];
        fa[x] = y;
        return true;
    }
};
 
struct Edge {
    int u, v, w;
    Edge() : Edge(0, 0, 0) {}
    Edge(int u, int v, int w) : u(u), v(v), w(w) {}
    bool operator<(const Edge& rhs) const {
        return w < rhs.w;
    }
};
 
const int N = 2500;
 
int g[N][N];
 
void solve() {
    int n;
    cin >> n;
 
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            cin >> g[i][j];
        }
    }
 
    vector<Edge> edge;
    for (int i = 0; i < n; ++i) {
        if (g[i][i]) {
            cout << "NOT MAGIC\n";
            return;
        }
        for (int j = i + 1; j < n; ++j) {
            if (g[i][j] != g[j][i]) {
                cout << "NOT MAGIC\n";
                return;
            }
            edge.emplace_back(i, j, g[i][j]);
        }
    }
 
    sort(edge.begin(), edge.end());
    DSU dsu(n);
    int last = -1;
    vector<PII> hav;
    for (auto& [u, v, w] : edge) {
        if (w != last) {
            last = w;
            while (hav.size()) {
                auto [x, y] = hav.back();
                dsu.merge(x, y);
                hav.pop_back();
            }
        }
        if (dsu.same(u, v)) {
            cout << "NOT MAGIC\n";
            return;
        }
        hav.emplace_back(u, v);
    }
    cout << "MAGIC\n";
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

柠檬味的橙汁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值