《信息学奥赛一本通·高手专项训练》集训 Day 1

贪心

20+20+100=140/Rank 7\color{Orange}20\color{Black}+\color{Orange}20\color{Black}+\color{Green}100\color{Black}=\color{#92E411}140\color{Black}\text{/Rank 7}

A. 小学数学\color{#F39C11}\text{A. 小学数学}

题目

czk 老板开了个饮料连锁店,连锁店共有 nn 家,出售的饮料种类相同。

为了促销,czk 决定让每家连锁店开展赠送活动。具体来说,在第 ii 家店,顾客可以用 aia_i 个饮料瓶兑换到 bib_i 瓶饮料和 11 个纪念币(注意不足 aia_i 个饮料瓶则不能兑换,若 ai=0a_i=0 则表示不需要任何瓶子就可以兑换)。

一家店可以兑换多次,兑换得到的饮料瓶还可以继续用于兑换。你买了 ss 瓶饮料,想知道用这 ss 瓶饮料最多可以兑换到多少个纪念币。

题解

显然,若存在 aibia_i\le b_i,可以无限换,输出相应的答案,但这种情况可以满足,还有一个前提条件:sais\ge a_i,有钱才有买的能力,我在这个地方挂了 30pts30\text{pts}

对于剩下的情况,我们肯定是希望一次交换能亏较少的瓶子,于是我们按照 aibia_i-b_i 从小到大排序,能换就尽量换,运用小学数学的知识可以推出换瓶子的式子。

但是,本题数字的范围高达 101910^{19},要开 unsigned long long\texttt{unsigned long long},我在代码中使用了除法,为了方便直接使用了浮点数向上取整,但这样做会因为各种各样的原因出错,还是得用有余数的除法来算,不然就会和我一样又挂 50pts50\text{pts}

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 1e5 + 10;
int n;
unsigned ll s, ans;
struct asdf {
    unsigned ll a, b;
} x[N];
bool cmp(asdf p, asdf q) { return p.a - p.b < q.a - q.b || (p.a - p.b == q.a - q.b && p.a < q.a); }
int main() {
    freopen("maths.in", "r", stdin);
    freopen("maths.out", "w", stdout);
    n = read();
    cin >> s;
    if (!n || !s) {
        puts("0");
        return 0;
    }
    for (int i = 1; i <= n; i++) {
        cin >> x[i].a >> x[i].b;
        if (x[i].a <= x[i].b && x[i].a <= s) {
            puts("-1");
            return 0;
        }
    }
    sort(x + 1, x + n + 1, cmp);
    for (int i = 1; i <= n; i++) {
        unsigned ll s1 = s % (x[i].a - x[i].b);
        if (s < x[i].a)
            continue;
        if (s1 < x[i].b)
            s1 += ((x[i].b - s1) / (x[i].a - x[i].b) + ((x[i].b - s1) % (x[i].a - x[i].b) != 0)) *
                  (x[i].a - x[i].b);
        ans += (s - s1) / (x[i].a - x[i].b);
        s = s1;
    }
    write(ans);
    return 0;
}

B. 黑白棋子\color{#FFC116}\text{B. 黑白棋子}

题目

有一个 nnmm 列的棋盘。棋盘的每个格子上要么是空,要么是一个黑色或白色的棋子。

我们现在将这个棋盘竖立起来,在游戏的任意时刻,这个棋盘都受到重力,满足不在最后一行的棋子下方都有棋子。

开始,棋盘上有一些棋子(但不一定是满的)。每一轮,玩家可以选择一个棋子 AA,然后将所有和棋子 AA 在同一列的同一颜色连续段的棋子 BB 全部消除(不包括 AA 本身)。所有 AABB 在同一颜色连续段即 AABB 颜色相同,且该列在 AABB 之间棋子均与 AA 同色。

若消除了至少一个棋子,那么消除成功,之后棋子 AA 本身将反色(黑色变白色,白色变黑色),接着所有棋子向下掉落直到棋子落到最下方为止(棋子的相对顺序不变);反之,如果没有棋子被消除,那么消除失败,棋盘不会有任何变化。

当所有棋子无法被消除时,游戏结束。 给定初始棋盘,请计算最少需要进行几轮消除才能使游戏结束。

题解

题目相当于 mm 个独立的询问,对每个询问进行“消消乐”,每次可以把一段长度大于 11 的连续 11 改成一个 00 或 连续 00 改为一个 11,直到不能再改为止。问消至不能消为止最少要几次?

先求出所有的颜色连续段和孤点,把他们都叫段,计所有段的个数为 tottot,然后大力分类讨论下:

  1. 没有连续段。不能消了,对答案没有影响。
  2. 有一个位于段序列正中间的连续段。那么所有段都可以化为一个孤点,答案为 tot2+1\left\lfloor\dfrac{tot}{2}\right\rfloor+1
  3. 所有连续段都在段序列的正中间的左边。此时找到左边最靠右的连续段进行消,设其位置为 ltlt,答案为 totlt+1tot-lt+1
  4. 所有连续段都在段序列的正中间的右边。此时找到右边最靠左的连续段进行消,设其位置为 rtrt,答案为 rtrt
  5. 段序列的正中间的左右两边都有连续段,同样找到 lt,rtlt,rt,若二者之间的距离过大,就算把 ltlt 左边和 rtrt 右边都消完也不如 rtlt+1rt-lt+1,那么答案为 rtlt+1rt-lt+1;若距离较小,则每次都是三个段合为一个段的速率减少,答案为 tot2+1\left\lfloor\dfrac{tot}{2}\right\rfloor+1

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 2e4 + 10, M = 210;
int n, m, ans, l[N], r[N], a[N], p[N], tot;
string s[M];
int main() {
    freopen("eliminate.in", "r", stdin);
    freopen("eliminate.out", "w", stdout);
    n = read();
    m = read();
    for (int i = 1; i <= m; i++) {
        cin >> s[i];
        tot = 0;
        for (int j = 0; j < s[i].size();) {
            p[++tot] = s[i][j] - '0';
            int k = j + 1;
            a[tot] = 1;
            while (k < s[i].size() && s[i][k] == s[i][j]) {
                a[tot]++;
                k++;
            }
            j = k;
        }
        bool f = 0;
        int lt = 0, rt = 0;
        for (int j = 1; j <= tot; j++) {
            if (a[j] > 1) {
                f = 1;
                if (j <= tot / 2)
                    lt = j;
                if (!rt && j > tot / 2)
                    rt = j;
            }
        }
        if (((tot & 1) && a[tot / 2 + 1] > 1))
            ans += tot / 2 + 1;
        else if (lt && !rt)
            ans += tot - lt + 1;
        else if (!lt && rt)
            ans += rt;
        else if (lt && rt)
            ans += max(rt - lt + 1, tot / 2 + 1);
    }
    cout << ans << endl;
    return 0;
}

C. 排队问题\color{#52C41A}\text{C. 排队问题}

题目

nn 个人排队,第 ii 个人身高为 aia_i(保证每个人身高不同) ,要么左边有 bib_i 个比她高的人,要么右边有 bib_i 个比她高的人。找出字典序最小的满足条件的身高序列。

题解

由于要字典序最小,于是我们把这些人按身高从小到大排序,一个一个考虑。

假设 ii 是身高最矮的人,他要在 nn 个位置中选一个位置,因为要字典序最小,所以他应该排在位置 min(bi+1,nbi)\min(b_i+1,n-b_i),若该位置不存在则无解。

接下来考虑身高第二矮的人,由于身高比其矮的人与他的位置没有关系,我们就可以抽象为身高最矮的人和他的位置一起被踢出了序列,其他位置的相对关系和编号不变,这样这个问题又转化为了上一个问题,一样求解。

为了维护空位的位置,我们可以直接用线段树,但因为他码量大,常数大,所以我选择在树状数组上二分,不仅好写而且常数小,复杂度仅多一只 log\text{log}

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 1e5 + 10;
int n, c[N];
ll w[N];
struct asdf {
    ll a;
    int b;
} x[N];
bool cmp(asdf p, asdf q) { return p.a < q.a; }
void add(int x, int v) {
    for (; x <= n; x += x & -x) c[x] += v;
}
int ask(int x) {
    int ans = 0;
    for (; x; x -= x & -x) ans += c[x];
    return ans;
}
int main() {
    freopen("queue.in", "r", stdin);
    freopen("queue.out", "w", stdout);
    n = read();
    for (int i = 1; i <= n; i++) {
        x[i].a = read();
        x[i].b = read();
        add(i, 1);
    }
    sort(x + 1, x + n + 1, cmp);
    for (int i = 1; i <= n; i++) {
        if (x[i].b > n - i + 1) {
            puts("impossible");
            return 0;
        }
        int rank = min(n - i + 1 - x[i].b, x[i].b + 1);
        int l = 1, r = n;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (ask(mid) >= rank)
                r = mid;
            else
                l = mid + 1;
        }
        w[l] = x[i].a;
        add(l, -1);
    }
    for (int i = 1; i <= n; i++) {
        cout << w[i] << " ";
    }
    cout << endl;
    return 0;
}

二分

10+100+25=140/Rank 8\color{Red}10\color{Black}+\color{Green}100\color{Black}+\color{Orange}25\color{Black}=\color{#FFC116}140\color{Black}\text{/Rank 8}

A.学习计划\color{#52C41A}\text{A.学习计划}

题目

小明很喜欢学习,他经常找同学借学习资料。

某天,小明心血来潮,制定了一个学习计划,计划上写着 nn 位同学的名字,依次编号为 1n1\sim n。每位同学有数量不等的资料。由于这天是周末,小明必须去这些同学的家里。

某些同学的家之间有单向通行的小路,通过每条小路的时间也不一定相同。现在,小明背着一个最多可以放 kk 份学习资料的书包,他决定先拜访编号为 11 的同学,走过若干条小路之后,来到编号为 nn 的同学的家。为了节省时间,他决定走到编号为 nn 的同学的家后,就不再拜访其他同学,而是直接回家。当小明在路上走的时候,每走 11 单位时间,他就会看完一份学习资料(如果还有学习资料)。每到一位同学的家(包括 11nn),他会将这位同学所有的学习资料放进他的背包,直到放满。

现在你的问题是:求 kk 的最小值,使得小明能够不浪费任何一份学习材料,即每到一位同学的家,他能把这个同学的所有学习资料放进背包。

题目保证小路构成一个有向无环图

题解

显然,kk 越大装得越多,所以可以二分,把求解转为判定,由于给出的是一个 DAG\text{DAG},拓扑排序一遍,并计算到每个点时背包装的最小的份数 did_i,如果背包最小份数仍大于 kk 就给他赋一个极大值让他不可能更新到 dnd_n,最后判断 dnd_n 是否小于等于 kk

代码

#include <bits/stdc++.h>
#define ll long long


using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 1e4 + 10, M = 3e4 + 10;
int n, m, t[N], l, r, d[N], in[N], ru[N];
int head[N], ver[M << 1], nxt[M << 1], edge[M << 1], tot;
void add(int x, int y, int z) {
    ver[++tot] = y;
    edge[tot] = z;
    nxt[tot] = head[x];
    head[x] = tot;
}
queue<int> q;
bool check(int k) {
    while (q.size()) q.pop();
    for (int i = 1; i <= n; i++) {
        in[i] = ru[i];
        d[i] = 0x3f3f3f3f;
        if (!in[i])
            q.push(i);
    }
    d[1] = t[1];
    while (q.size()) {
        int x = q.front();
        q.pop();
        if (k < d[x])
            d[x] = 0x3f3f3f3f;
        for (int i = head[x]; i; i = nxt[i]) {
            int y = ver[i];
            in[y]--;
            d[y] = min(d[y], t[y] + max(d[x] - edge[i], 0));
            if (!in[y])
                q.push(y);
        }
    }
    return k >= d[n];
}
int main() {
    freopen("peach.in", "r", stdin);
    freopen("peach.out", "w", stdout);
    n = read();
    m = read();
    for (int i = 1; i <= n; i++) {
        t[i] = read();
        r += t[i];
    }
    for (int i = 1; i <= m; i++) {
        int a, b, c;
        a = read();
        b = read();
        c = read();
        add(a, b, c);
        ru[b]++;
    }
    while (l < r) {
        int mid = (l + r) >> 1;
        if (check(mid))
            r = mid;
        else
            l = mid + 1;
    }
    write(l);
    return 0;
}

B.生命游戏\color{#52C41A}\text{B.生命游戏}

题目

小可可在玩一款简化的生命游戏。

游戏初始有若干格子有生命存在,如果在某一秒一个不存在生命的格子上下左右任意一个格子存在生命,那么这个格子下一秒也会存在生命。已经存在的生命不会死亡。

现在小可可想让生命摆成一种特定的图案。他想知道,初始时如果他放置可以任意放置生命,那么最多经过多少秒,有生命的格子才会摆成小可可想要的图案呢?(必须在某一秒要摆出来才行)

题解

我的解法和答案完全不同,复杂度也不是很对,不过还是过了,而且码长只有标程的一半多。

首先肯定是二分最多的秒数 kk,我是尝试在每个最后有生命的点放初始的生命,为了方便直接把它当做生命伸展后的顶部的顶点,伸展到的图形显然是个类似菱形的图形。如果放了后能完全被结果覆盖住,那么就放下去,并把覆盖到的点打上标记。

然后判定是否每个最后有生命点都被打上了标记就能得到判定结果。

直接这样做时间复杂度是 O(nmk2logn)O(nmk^2\log n) 的,最多只有 30pts30\text{pts},我们可以把枚举的范围缩小进行小小的优化,然后发现代码中有对结果数组的一维区间查询操作,和对标记数组的一维区间修改操作,于是我一时兴起写了个线段树维护,时间复杂度变为 O((n2k)(m2k)klogklogn)O((n-2k)(m-2k)k\log k\log n),然而分数没变。

后来我发现其实对两个数组可以分别用前缀和(离线修改,在线查询)和差分(在线修改,离线查询),于是时间复杂度变为 O((n2k)(m2k)klogn)O((n-2k)(m-2k)k\log n),砍掉一只 log\log 后过了。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 1010;
int n, m, b[N][N], a[N][N];
char c[N][N];
bool check(int k) {
    memset(b, 0, sizeof(b));
    for (int i = 2; i + k + 1 <= n; i++) {
        for (int j = k + 2; j + k + 1 <= m; j++) {
            bool f = 0;
            for (int p = 0; p <= k; p++) {
                if (a[p + i][j + p] - a[p + i][j - p - 1] != 2 * p + 1) {
                    f = 1;
                    break;
                }
            }
            if (f)
                continue;
            for (int p = 1; p <= k; p++) {
                if (a[p + i + k][j + (k - p)] - a[p + i + k][j - (k - p) - 1] != 2 * k - 2 * p + 1) {
                    f = 1;
                    break;
                }
            }
            if (f)
                continue;
            for (int p = 0; p <= k; p++) {
                b[p + i][j - p]++;
                b[p + i][j + p + 1]--;
            }
            for (int p = 1; p <= k; p++) {
                b[p + i + k][j - (k - p)]++;
                b[p + i + k][j + (k - p) + 1]--;
            }
        }
    }
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++) {
            b[i][j] += b[i][j - 1];
            if (c[i][j] == '#' && !b[i][j]) {
                return 0;
            }
        }
    return 1;
}
int main() {
    freopen("game.in", "r", stdin);
    freopen("game.out", "w", stdout);
    n = read();
    m = read();
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> c[i][j];
            a[i][j] = a[i][j - 1] + (c[i][j] == '#');
        }
    }
    int l = 0, r = min(n, m) / 2;
    while (l < r) {
        int mid = (l + r + 1) >> 1;
        if (check(mid))
            l = mid;
        else
            r = mid - 1;
    }
    write(l);
    return 0;
}

C.三连击杀\color{#3498DB}\text{C.三连击杀}

题目

nn 个怪物,第 ii 只怪物的血量为 bloodiblood_i

你现在有技能「三连击杀」,每使用一次这个技能,你都可以选择至少一个、至多三个不同的怪物,并任意决定这几个怪物的击杀顺序,产生如下伤害:

  1. 使第一个怪物的血量减少 99
  2. 如果选了第二个怪物,则使第二个怪物的血量减少 33
  3. 如果选了第三个怪物,则使第三个怪物的血量减少 11

当怪物的血量减少到小于或等于 00 时,则该怪物死亡。

你现在需要求出,最少使用多少次技能,能使所有怪物死亡。

题解

仍然二分答案 xx,在判定内部 DP\text{DP},设 fi,j,kf_{i,j,k} 表示前 ii 个妖怪,用了 jj 次伤害为 99 的攻击和 kk 次伤害为 33 的攻击,至少要用几次伤害为 11 的攻击。

转移有两种:

  1. 使用 pp 次伤害为 99 的攻击和 qq 次伤害为 33 的攻击不能消灭第 ii 个妖怪:
fi,j,k=fi1,jp,kq+bloodi9×p3×q (bloodi>9×p+3×q,p+qx,pj,qk)f_{i,j,k}=f_{i-1,j-p,k-q}+blood_i-9\times p-3\times q\ (blood_i>9\times p+3\times q,p+q\le x,p\le j,q\le k)
  1. 使用 pp 次伤害为 99 的攻击和 qq 次伤害为 33 的攻击不能消灭第 ii 个妖怪:
fi,j,k=fi1,jp,kq (bloodi9×p+3×q,p+qx,pj,qk)f_{i,j,k}=f_{i-1,j-p,k-q}\ (blood_i\le 9\times p+3\times q,p+q\le x,p\le j,q\le k)

最后判定 min{fn,j,k}mid\min\{f_{n,j,k}\}\le mid 是否成立即可。

对于总共 1201\sim 20 个妖怪的最大攻击次数可以提前预处理出来,优化常数。

代码

#include <bits/stdc++.h>
#define ll long long

using namespace std;
long long read() {
    long long x = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) {
        if (ch == '-')
            f = -1;
        ch = getchar();
    }
    while (isdigit(ch)) {
        x = x * 10 + ch - 48;
        ch = getchar();
    }
    return x * f;
}
void write(long long x) {
    if (x < 0)
        putchar('-'), x = -x;
    if (x > 9)
        write(x / 10);
    putchar(x % 10 + '0');
}
const int N = 110;
int R, n, b[N], f[N][N][N];
int mx[21] = { 0, 7, 10, 14, 19, 24, 28, 33, 37, 42, 47, 51, 56, 60, 65, 70, 74, 79, 84, 88, 93 };
bool check(int x) {
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= x; j++) {
            for (int k = 0; k <= x; k++) {
                f[i][j][k] = 0x3f3f3f3f;
            }
        }
    }
    f[0][0][0] = 0;
    for (int i = 1; i <= n; i++) {
        for (int p = 0; p <= min(mx[i], x); p++) {
            for (int q = 0; p + q <= min(mx[i], x); q++) {
                if (b[i] <= p * 9 + q * 3) {
                    for (int j = p; j <= min(mx[i], x); j++)
                        for (int k = q; k <= min(mx[i], x); k++)
                            f[i][j][k] = min(f[i][j][k], f[i - 1][j - p][k - q]);
                    break;
                } else if (x >= p + q + b[i] - p * 9 - q * 3)
                    for (int j = p; j <= x; j++)
                        for (int k = q; k <= x; k++)
                            f[i][j][k] = min(f[i][j][k], f[i - 1][j - p][k - q] + b[i] - p * 9 - q * 3);
            }
        }
    }
    int res = 0x3f3f3f3f;
    for (int i = 0; i <= x; i++)
        for (int j = 0; j <= x; j++) res = min(res, f[n][i][j]);
    return res <= x;
}
int main() {
    freopen(".in", "r", stdin);//不要问我问什么这个文件名这么怪
    freopen(".out", "w", stdout);
    R = read();
    while (R--) {
        n = read();
        for (int i = 1; i <= n; i++) {
            b[i] = read();
        }
        int l = 1, r = 93;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (check(mid))
                r = mid;
            else
                l = mid + 1;
        }
        write(l);
        puts("");
    }
    return 0;
}
posted @ 2022-08-02 07:44  luckydrawbox  阅读(79)  评论(0)    收藏  举报  来源