【简单算法】2021SCUACM集训队冬季选拔2全题解

引言

传送门

本来在预习课程期末的,结果还是没能忍住来写了一套题

20230416更新:终于把之前没写的题都补上了~

本文juejin:https://juejin.cn/post/7222663980391759928/

本文CSDN:https://blog.csdn.net/hans774882968/article/details/122158687

作者:hans774882968以及hans774882968以及hans774882968

A-签到

签到。数有多少个个位为9的。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 3e5 + 5;

int n;

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

int main (int argc, char **argv) {
    int T;
    read (T);
    while (T--) {
        read (n);
        printf ("%d\n", (n + 1) / 10);
    }
    return 0;
}

B-多重背包

看到m <= 1000就猜到大约是m^2的算法,所以显然要先取模+开桶计数。把模m的和看成重量,于是转化为多重背包求是否存在方案的问题。

但麻烦的是,不能直接用dp[m][0],因为它受到dp[0][0] = true的影响,必然为true;并且也无法直接算方案数。那么我们就在dp转移的过程中监听即可。

不难的一个背包居然tag是1900

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 1e3 + 5;

int n, m, c[N];
bool dp[N * 15][N];

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

void pack (int &tot, bool &ans, int v, int w) {
    if (!v) return;
    ++tot;
    dwn (j, m - 1, 0) {
        dp[tot][j] = dp[tot - 1][j] || dp[tot - 1][ (j - w + m) % m];
        if (!j) ans |= dp[tot - 1][ (j - w + m) % m];
    }
}

int main (int argc, char **argv) {
    read (n, m);
    rep (i, 1, n) {
        int x;
        read (x);
        c[x % m + 1]++;
    }
    dp[0][0] = true;
    int tot = 0;
    bool ans = false;
    rep (i, 1, m) {
        int u = c[i];
        for (int j = 0; (1 << j) < u; ++j) {
            pack (tot, ans, 1 << j, (1 << j) * (i - 1) % m);
            u -= (1 << j);
        }
        pack (tot, ans, u, u * (i - 1) % m);
    }
    puts (ans ? "YES" : "NO");
    return 0;
}

C-分类讨论

签到。简单分类讨论。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 3e5 + 5;

int n;

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

int main (int argc, char **argv) {
    int T;
    read (T);
    while (T--) {
        read (n);
        int s = 0;
        rep (i, 1, n) {
            int x;
            read (x);
            s += x;
        }
        if (s >= n) printf ("%d\n", s - n);
        else puts ("1");
    }
    return 0;
}

D-CF682D-预处理最长公共后缀+前缀定义dp

首先能想到一个前缀定义dp:dp[i][j1][j2]表示考虑了字符串s[1~j1], t[1~j2](1-indexed),选择了i个相同子串的最大长度和。状态转移方程:

  1. 不选择公共后缀的情况有2种:max(dp[i][j1-1][j2], dp[i][j1][j2-1])
  2. 选择公共后缀时,直接贪心地选择最长公共后缀即可:dp[i-1][j1-suf[j1][j2]][j2-suf[j1][j2]] + suf[j1][j2]

最长公共后缀可以O(n * m)dp预处理:suf[i][j] = s[i] == t[j] ? suf[i - 1][j - 1] + 1 : 0

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> pii;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 1e3 + 5;

int n, m, k, dp[11][N][N], suf[N][N];
char s[N], t[N];

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

int main() {
    read (n, m, k);
    scanf ("%s", s + 1);
    scanf ("%s", t + 1);
    rep (i, 1, n) {
        rep (j, 1, m) {
            suf[i][j] = s[i] == t[j] ? suf[i - 1][j - 1] + 1 : 0;
        }
    }
    rep (i, 1, k) {
        rep (j1, 1, n) {
            rep (j2, 1, m) {
                dp[i][j1][j2] = max (dp[i][j1 - 1][j2], dp[i][j1][j2 - 1]);
                if (!suf[j1][j2]) continue;
                dp[i][j1][j2] = max (dp[i][j1][j2], dp[i - 1][j1 - suf[j1][j2]][j2 - suf[j1][j2]] + suf[j1][j2]);
            }
        }
    }
    printf ("%d\n", dp[k][n][m]);
    return 0;
}

E-组合数学经典模型

一看就很组合数学,而且没法dp。突破口就是令一些房间为空。于是非空的房间相当于经典的方程求解的个数问题,如果没听说过,可以参考我的这篇介绍

枚举空房间个数i = 0~min(n-1,k),则C(n,i)选出空房间,C(n-i+i-1,i)表示n-i个变量,和为i的方案数,乘起来就是贡献。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 3e5 + 5, mod = 1e9 + 7;

int n, k;
LL fac[N], ifac[N];

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

LL q_pow (LL a, LL b, LL mod) {
    LL ret = 1;
    for (; b; b >>= 1) {
        if (b & 1) ret = ret * a % mod;
        a = a * a % mod;
    }
    return ret;
}

void init (int n) {
    fac[0] = 1;
    rep (i, 1, n) fac[i] = fac[i - 1] * i % mod;
    ifac[n] = q_pow (fac[n], mod - 2, mod);
    dwn (i, n - 1, 0) ifac[i] = (i + 1) * ifac[i + 1] % mod;
}

LL C (int x, int y) {
    if (x < y) return 0;
    return fac[x] * ifac[y] % mod * ifac[x - y] % mod;
}

int main (int argc, char **argv) {
    read (n, k);
    init (n);
    int ans = 0;
    rep (i, 0, min (n - 1, k) )
        ans = (ans + C (n, i) * C (n - 1, i) % mod) % mod;
    printf ("%d\n", ans);
    return 0;
}

F-贪心

贪心依据是排序不等式。贪心过程需要一个数据结构,维护当前可选但未选的下标,支持插入、删除和求最小权值。这个数据结构就是优先队列。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 3e5 + 5;

int n, k, a[N];

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

int main (int argc, char **argv) {
    read (n, k);
    rep (i, 1, n) read (a[i]);
    priority_queue<pair<int, int>> q;
    rep (i, 1, k) q.push ({a[i], i});
    LL tot = 0;
    vector<int> ans (n);
    rep (i, k + 1, k + n) {
        if (i <= n) q.push ({a[i], i});
        pair<int, int> u = q.top();
        q.pop();
        tot += 1LL * u.first * (i - u.second);
        ans[u.second - 1] = i;
    }
    printf ("%lld\n", tot);
    re_ (i, 0, n) cout << ans[i] << " \n"[i == n - 1];
    return 0;
}

G-最短路拼接

最短路拼接这个技巧都忘了QAQ。暴力做法就是直接枚举所有边,如果当前边连上后,经过当前边的s到t的最短路比现有的更短,那么当前边不合法;否则合法。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 1e3 + 5;

int n, m, s, t, a[N][N];

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

void bfs (int s, vector<int> &dis) {
    queue<int> q;
    q.push (s);
    dis[s] = 0;
    while (!q.empty() ) {
        int u = q.front();
        q.pop();
        rep (v, 1, n) if (a[u][v] && (!~dis[v]) ) {
            dis[v] = dis[u] + 1;
            q.push (v);
        }
    }
}

int main (int argc, char **argv) {
    read (n, m, s, t);
    rep (i, 1, m) {
        int u, v;
        read (u, v);
        a[u][v] = a[v][u] = true;
    }
    vector<int> dis1 (n + 1, -1), dis2 (n + 1, -1);
    bfs (s, dis1);
    bfs (t, dis2);
    int ans = 0;
    rep (i, 1, n) rep (j, i + 1, n) if (!a[i][j]) {
        int d = min (dis1[i] + 1 + dis2[j], dis1[j] + 1 + dis2[i]);
        ans += (d >= dis1[t]);
    }
    printf ("%d\n", ans);
    return 0;
}

H-CF1070C-权值树状数组O(nlog^2n),也可以用权值线段树O(nlogn)

显然某两天之间没有影响,所以我们只需要单独优化每一天的选择即可。

对于某一天,我们只需要一个简单的贪心:不断取当天提供的最便宜的CPU,直到个数达到k个或取完。但暴力是O(n*m)的。因此不难想到需要维护数据结构的差分,即当天与前一天的差异。我们引入一个struct Op {int typ, c, p;};,和一个vector<Op> op[N]typ = 1表示当天要加入一个计划,typ = -1表示当天要删除一个计划。代码如下:

    rep (i, 1, m) {
        int l, r, c, p;
        read (l, r, c, p);
        op[l].push_back ({1, c, p});
        op[r + 1].push_back ({-1, c, p});
    }

接下来考虑使用什么数据结构。因为要按照价格升序贪心,所以我们以价格为下标,开两个桶C1[v], C2[v]分别表示价格为v的CPU总数和总价格。于是我们需要找到最小的价格mn_idx使得sum(C1[mn_idx]) >= k, sum(C1[mn_idx-1]) < k。则这一天的总价格就是sum(C2[mn_idx-1]) + (k - sum(C1[mn_idx-1])) * mn_idx。这两个桶的操作是单点修改和求前缀和,因此可以用树状数组。但是求mn_idx的朴素做法就需要二分答案了,复杂度就有两个log,幸好CF还是让我过了。

可以用权值线段树来避免二分答案的操作,参考:https://www.cnblogs.com/wushansinger/p/15750956.html。线段树的下标仍定义为价格,维护cnt, cost属性表示价格在这个区间内的CPU总个数和总价格。这时,贪心策略在线段树的体现就是:优先遍历左侧节点,左侧足够了就不必遍历右侧节点。涉及的操作:单点修改、区间查询。如何处理CPU不够的问题?参考链接的做法是在线段树之外进行判定。这个做法的思想和上述权值树状数组的做法一样朴素,我就懒得写代码了。

还有一种叫“树状数组上倍增”的做法:https://www.luogu.com.cn/blog/FrozaFerrari/solution-cf1070c。这种做法也是一个log,我看不懂,等一个佬来教教我~

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int, int> pii;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)
#define lowbit(x) ((x) & (-(x)))

const int N = 1e6 + 5;

int n, m, k, mx_p = 0;
LL C1[N], C2[N];

struct Op {
    int typ, c, p;
};
vector<Op> op[N];

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

void mdy (LL C[], int x, LL v) {
    for (; x <= mx_p; x += lowbit (x) ) C[x] += v;
}

LL qry (LL C[], int x) {
    LL ans = 0;
    for (; x; x -= lowbit (x) ) ans += C[x];
    return ans;
}

int main() {
    read (n, k, m);
    rep (i, 1, m) {
        int l, r, c, p;
        read (l, r, c, p);
        op[l].push_back ({1, c, p});
        op[r + 1].push_back ({-1, c, p});
        mx_p = max (mx_p, p);
    }
    LL ans = 0;
    rep (i, 1, n) {
        for (auto x : op[i]) {
            mdy (C1, x.p, x.typ * x.c);
            mdy (C2, x.p, 1LL * x.typ * x.c * x.p);
        }
        int l = 1, r = mx_p;
        int goal = min (1LL * k, qry (C1, mx_p) );
        while (l < r) {
            int mid = (l + r) >> 1;
            LL val = qry (C1, mid);
            if (val >= goal) r = mid;
            else l = mid + 1;
        }
        int mn_idx = l;
        ans += qry (C2, mn_idx - 1) + (goal - qry (C1, mn_idx - 1) ) * mn_idx;
    }
    printf ("%lld\n", ans);
    return 0;
}

I-二分答案+树状数组

看到“最小值最大化”就先试试二分ans。怎么贪呢?考虑端点。最靠左的小于x的数是必须变成x的。那么我们选取的区间一定要尽量靠右,因为它左边的数都不需要被增加。

这时我们需要一个数据结构支持区间加、求单点值。这就是差分数组,用树状数组维护。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)
#define lowbit(x) ((x) & (-(x)))

const int N = 3e5 + 5;

int n, m, w, a[N], C[N];

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

void mdy (int x, int v) {
    for (; x <= n; x += lowbit (x) ) C[x] += v;
}

int qry (int x) {
    int ans = 0;
    for (; x; x -= lowbit (x) ) ans += C[x];
    return ans;
}

bool jdg (int x) {
    rep (i, 1, n) C[i] = 0;
    rep (i, 1, n) mdy (i, a[i]), mdy (i + 1, -a[i]);
    int u = 0;
    rep (i, 1, n) {
        int v = qry (i);
        if (v >= x) continue;
        u += x - v;
        if (u > m) return false;
        mdy (i, x - v);
        mdy (i + w, v - x);
    }
    return true;
}

int main (int argc, char **argv) {
    read (n, m, w);
    rep (i, 1, n) read (a[i]);
    int l = *min_element (a + 1, a + n + 1), r = *max_element (a + 1, a + n + 1) + m;
    while (l < r) {
        int mid = (l + r + 1) >> 1;
        if (jdg (mid) ) l = mid;
        else r = mid - 1;
    }
    printf ("%d\n", l);
    return 0;
}

J-利用树的直径相关结论

这个构造题不难啊?但过的人不够多……

首先需要一个树的直径的结论:设一个离根最远的点为u,以u为根求出一个离u最远的点v。则(u,v)路径是其中一个树的直径。于是我们构造1 ~ h+1是一条链,而h+2 ~ d+1是1号点的另一个子树(另一条链),然后剩下的点全部放到h号点下面。

值得注意的是一些特判:2*h < d无解,d == 1无法构造两棵子树。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)

const int N = 3e5 + 5;

int n, d, h;
vector<int> G[N];
vector<pair<int, int>> ans;

void dbg() {
    puts ("");
}
template<typename T, typename... R>void dbg (const T &f, const R &... r) {
    cout << f << " ";
    dbg (r...);
}
template<typename Type>inline void read (Type &xx) {
    Type f = 1;
    char ch;
    xx = 0;
    for (ch = getchar(); ch < '0' || ch > '9'; ch = getchar() ) if (ch == '-') f = -1;
    for (; ch >= '0' && ch <= '9'; ch = getchar() ) xx = xx * 10 + ch - '0';
    xx *= f;
}
void read() {}
template<typename T, typename ...R>void read (T &x, R &...r) {
    read (x);
    read (r...);
}

void dfs (int u, int ufa) {
    if (ufa) ans.push_back ({ufa, u});
    for (int v : G[u]) dfs (v, u);
}

int main (int argc, char **argv) {
    read (n, d, h);
    if (2 * h < d) {
        puts ("-1");
        return 0;
    }
    if (d == 1 && n > 2) {
        puts ("-1");
        return 0;
    }
    rep (i, 1, h) G[i].push_back (i + 1);
    rep (i, h + 1, d) G[i == h + 1 ? 1 : i].push_back (i + 1);
    rep (i, d + 2, n) G[h].push_back (i);
    dfs (1, 0);
    re_ (i, 0, n - 1) printf ("%d %d\n", ans[i].first, ans[i].second);
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值