牛客刷题-Day20

牛客刷题-Day20

今日刷题:\(1021-1030\)

1023 枚举 · 例20-【模板】子序列自动机Ⅰ|子序列检查

c658eda8-f126-474f-bd43-aa3b056f7d3d

解题思路

枚举、二分。
预处理字符串 \(s\),记录每个出现的小写字母的个数以及出现的位置。
对于当前的字符串 \(t\),先进行特殊情况的判断,一是其长度不能大于 \(s\),二是其中的每个小写字母出现的次数不能大于 \(s\) 中对应字母出现的次数。
排除上述两种情况之后,对于当前的字符串 \(t\) 的一个字母 \(t_i\),其在字符串 \(s\) 中出现的位置要比其前一个字母出现的位置靠后,假设前一个字母 \(t_{i-1}\)\(s\) 中出现的位置为 \(last\),则 \(t_i\) 的位置至少为 \(last+1\) 方才成立,在 \(t_i\) 对应的存储位置的数组中找到最小的不小于 \(last+1\) 的元素(符合要求的情况下尽量靠近前一个字母出现的位置),更新 \(last\) 为该值。
使用 \(last+1\) 可以避免相邻两个元素为同一个字母的情况,如果使用 \(last\) 作为比较对象,则查找到的可能会重合。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10, M = 26;

int q;
string s;
vector<int> pos[M]; // pos[i] 存储字母 i 的位置 pos[i][0] 存储 i 的个数
int cnt[M];

int main() {
    cin >> s;
    for (int i = 0; i < M; i++)
        pos[i].push_back(0);
    for (int i = 0; s[i]; i++) { // 初始化
        pos[s[i] - 'a'][0]++;
        pos[s[i] - 'a'].push_back(i);
    }
    cin >> q;
    while (q--) {
        memset(cnt, 0, sizeof cnt);
        string t;
        cin >> t;
        if (t.size() > s.size()) { // 比原串长
            puts("NO");
            continue;
        }
        bool flag = true;
        for (int i = 0; t[i]; i++)
            cnt[t[i] - 'a']++;
        for (int i = 0; i < M; i++)
            if (cnt[i] > pos[i][0]) { // t 中某个字母个数比 s 的多
                flag = false;
                break;
            }
        if (!flag) {
            puts("NO");
            continue;
        }
        int last = -1;
        for (int i = 0; t[i]; i++) {
            int l = 1, r = pos[t[i] - 'a'].size() - 1;
            while (l < r) {
                int mid = l + r >> 1;
                if (pos[t[i] - 'a'][mid] >= last + 1)
                    r = mid;
                else
                    l = mid + 1;
            }
            if (pos[t[i] - 'a'][l] >= last + 1)
                last = pos[t[i] - 'a'][l];
            else {
                flag = false;
                break;
            }
        }
        if (!flag) {
            puts("NO");
        } else {
            puts("YES");
        }
    }
    return 0;
}

1024 贪心 · 例2-拼数

a9947bdd-8495-4f9e-be26-105e565cec18

解题思路

排序。
重写一下排序函数:

  • 从高位往低位比较,大的在前面;
  • 其中一个为另一个的前缀子串,则比较 \(a+b\)\(b+a\) 的大小。

对于第二种情况,举个栗子:\(51\)\(510\),那么拼接要么是 \(51510\) 或者 \(51051\)

bool cmp(string a, string b) {
    for (int i = 0; i < a.size() && i < b.size(); i++) {
        if (a[i] != b[i])
            return a[i] > b[i];
    }
    return a + b > b + a;
}

C++ 代码

#include <bits/stdc++.h>
using namespace std;
const int N = 25;

int n, a[N];
string s[N];

bool cmp(string a, string b) {
    for (int i = 0; i < a.size() && i < b.size(); i++) {
        if (a[i] != b[i])
            return a[i] > b[i];
    }
    return a + b > b + a;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        s[i] = to_string(a[i]);
    }
    sort(s + 1, s + n + 1, cmp);
    for (int i = 1; i <= n; i++)
        cout << s[i];
    return 0;
}

1025 贪心 · 例3-给定长度和数位和

9ddb42b9-8e95-421f-9fed-d0a37c3a7b4d

解题思路

贪心。
首先对特殊情况进行特判。然后,如果是最小的,则从后往前确定,末尾数字尽可能为 \(9\),不够 \(9\),则当前位置为剩下的 \(s-1\),最高位为 \(1\),因为不能存在前导零。如果是最大的,从前往后确定,前面的数字尽可能为 \(9\),不够则当前位置为剩下的 \(s\),后续位置为 \(0\) 即可。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
const int N = 110;

int T;
int num[N];

int main() {
    cin >> T;
    while (T--) {
        int m, s;
        cin >> m >> s;
        if (s > m * 9 || (s == 0 && m > 1))
            cout << "-1" << endl;
        else if (s == 0 && m == 1)
            cout << "0 0" << endl;
        else {
            int len = m, tot = s;
            while (tot > 0 && len >= 1) {
                if (tot > 9) num[len--] = 9, tot = tot - 9;
                else {
                    num[len] = tot - 1, num[1]++;
                    break;
                }
            }
            for (int i = 1; i <= m; i++) 
                cout << num[i], num[i] = 0;
            cout << ' ';
            len = 1, tot = s;
            while (tot > 0 && len <= m) {
                if (tot > 9) num[len++] = 9, tot = tot - 9;
                else num[len++] = tot, tot = 0;
            }
            for (int i = 1; i <= m; i++) 
                cout << num[i], num[i] = 0;
                    cout << endl;
        }
    }
    return 0;
}

1026 贪心 · 例4-排座椅

d8f033df-8c89-4560-95ac-8e8c210b1262

解题思路

取最多的行和列进行分隔。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;

int n, m, k, l, d;
struct Node {
    int idx, cnt;
} row[N], col[N];

bool cmp1(Node a, Node b) {
    return a.cnt > b.cnt;
}

bool cmp2(Node a, Node b) {
    return a.idx < b.idx;
}

int main() {
    scanf("%d%d%d%d%d", &n, &m, &k, &l, &d);
    for (int i = 1; i <= n; i++)
        row[i].idx = i;
    for (int i = 1; i <= m; i++)
        col[i].idx = i;
    while (d--) {
        int x, y, p, q;
        scanf("%d%d%d%d", &x, &y, &p, &q);
        if (x == p) col[min(y, q)].cnt++; // 左右相邻
        else row[min(x, p)].cnt++; // 上下相邻
    }
    sort(row + 1, row + n + 1, cmp1);
    sort(col + 1, col + m + 1, cmp1);
    
    sort(row + 1, row + k + 1, cmp2);
    sort(col + 1, col + l + 1, cmp2);
    
    for (int i = 1; i <= k; i++)
        printf("%d ", row[i].idx);
    printf("\n");
    for (int i = 1; i <= l; i++)
        printf("%d ", col[i].idx);
    printf("\n");
    return 0;
}

1027 贪心 · 例5-矩阵消除游戏

4b72b46a-11ea-4612-9710-3d32cd028f28

解题思路

最初的想法是先求每行、每列的和,然后寻找最大的行或者列,删除之后重新计算,再求解一次,循环往复。但是这样是错误的,因为每次删除之后会对后面的选择产生影响。举个例子:

100 9 1
  0 0 0
100 8 0

假设只能取两次,这样取会先取第一列,清零之后然后取第一行,和为 \(100+100+9+1=210\),但是如果直接取第一行和第三行,和为 \(100+9+1+100+8=218\),反而更优。
因此,先固定选择行的方案,然后对于列,取部分列的和最大的。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
const int N = 20;
typedef long long LL;

int n, m, k;
int a[N][N], col[N];

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

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            scanf("%d", &a[i][j]);
    int ans = 0;
    for (int op = 0; op < (1 << n); op++) { // 固定行的选择
        // 初步统计每列的和
        memset(col, 0, sizeof col);
        for (int j = 1; j <= m; j++)
            for (int i = 1; i <= n; i++)
                col[j] += a[i][j];
        if (cnt1(op) > k)
            continue;
        int r = op, idx = 1, res = 0;
        while (r) {
            if (r & 1) { // 取该行
                for (int j = 1; j <= m; j++) {
                    res += a[idx][j];
                    col[j] -= a[idx][j];
                }
            }
            r >>= 1;
            idx++;
        }
        sort(col + 1, col + m + 1);
        for (int i = 0; i < min(k - cnt1(op), m); i++)
            res += col[m - i];
        ans = max(ans, res);
    }
    printf("%lld\n", ans);
    return 0;
}

1028 贪心 · 例7-活动安排

解题思路

反证法:假设有一个最优解,但没有按照截止时间排序。如果交换两个活动的位置,看看是否会导致扣分减少。
假设有两个活动 \(A = (c_A, d_A)\)\(B = (c_B, d_B)\),且 \(A\) 的截止时间比 \(B\) 早,即 \(d_A < d_B\)。但是在最优解中,先做了 \(B\),然后做了 \(A\),从而导致 \(A\) 的完成时间超过了 \(d_A\),产生了扣分。我们来分析交换这两个活动顺序的效果。
交换顺序前:
完成活动 \(B\) 后的时间是 \(t_B = c_B\)
然后进行活动 \(A\),其完成时间是 \(t_A = t_B + c_A = c_B + c_A\)
如果 \(t_A > d_A\),则扣分为 \(t_A - d_A\)
交换顺序后:
如果先做活动 \(A\),完成时间为 \(t_A = c_A\)
然后进行活动 \(B\),其完成时间是 \(t_B = t_A + c_B = c_A + c_B\)
如果 \(t_B > d_B\),则扣分为 \(t_B - d_B\)
通过交换活动顺序,\(A\) 的完成时间没有增加,而 \(B\) 的完成时间可能会变大。由于 \(d_A < d_B\),活动 \(A\) 的扣分更敏感。因此,在最优解中,如果交换顺序,\(A\) 的扣分不会增加,而 \(B\) 的扣分有可能增加。因此,如果不按照截止时间排序,活动的顺序可能导致更大的扣分,且无法得到更小的最大扣分。
结论:按照活动的截止时间 \(d_i\) 排序,优先完成截止时间较早的活动,是最优的策略。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
typedef long long LL;

int n;
struct Node {
    int c, d;
} a[N];

bool cmp(Node a, Node b) {
    return a.d < b.d;
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &a[i].c, &a[i].d);
    sort(a + 1, a + n + 1, cmp);
    LL current = 0, res = 0;
    for (int i = 1; i <= n; i++) {
        current += a[i].c;
        res = max(res, current - a[i].d);
    }
    printf("%lld\n", res);
    return 0;
}

1030 贪心 · 例9-保护那些花

538e085a-4150-4e13-a86b-cad633aae985

解题思路

设牛的集合为 \((C={1,2,\dots,n})\)。每头牛 \((i\in C)\) 有两个参数:距离 \((t_i>0)\)(分钟)和吃花速度 \((d_i\ge0)\)(朵/分钟)。
对一个排列(运输顺序) \(\pi=(\pi(1),\pi(2),\dots,\pi(n))\)(这是 \((C)\) 的一个线性序列),定义第 \(k\) 个被运走的牛为 \(\pi(k)\)。当执行到第 \(k\) 头牛开始被运走时,已经过去的时间为

\[T_{k-1}=2\sum_{u=1}^{k-1} t_{\pi(u)} \]

(每次往返花 \(2t\),被运走的牛在被运走期间不再吃花)。
因此被安排在位置 \(k\) 的牛 \(\pi(k)\) 在开始运输前已经吃掉的花数为

\[\text{eat}_{\pi(k)} = d_{\pi(k)}\cdot T_{k-1}. \]

总体被吃掉的花为

\[F(\pi)=\sum_{k=1}^n d_{\pi(k)}T_{k-1} =2\sum_{k=1}^n d_{\pi(k)}\sum_{u=1}^{k-1} t_{\pi(u)} =2\sum_{1\le u<k\le n} t_{\pi(u)},d_{\pi(k)} \]

为简洁起见,把常数 \(2\) 暂时忽略(不会影响最优顺序)。所以要最小化

\[G(\pi)=\sum_{1\le u<k\le n} t_{\pi(u)}d_{\pi(k)} \]

定义二元关系 \(\prec\)\(C\) 上:对于两头不同的牛 \((i,j)\)

\[i \prec j \quad\iff\quad t_i d_j < t_j d_i. \]

等号时可按任意稳定规则断开平局,例如按编号。注意 \(i\prec j\) 等价于 \(\dfrac{t_i}{d_i} < \dfrac{t_j}{d_j}\)(当 \(d_i,d_j>0\) 时)。

证明最优序列 \(\pi\) 必须没有违反关系 \(\prec\) 的逆序(即若 \(i\prec j\)\(i\) 不会排在 \(j\) 之后)。
只看某个相邻对(位置 \(p\)\(p+1\)),设在某序列中相邻两头牛为 \(a\)(在前)和 \(b\)(在后)。只考虑这对对总花损的影响,其它牛不变。对这对:

  • 若顺序为 \((\dots,a,b,\dots)\),这对产生的贡献是 \(t_a d_b\)(因为只在 \(u<k\) 且恰为这一对时产生交叉项)。
  • 若顺序改为 \((\dots,b,a,\dots)\),这对产生的贡献变为 \(t_b d_a\)
    两种顺序的差值为

\[\Delta = (t_a d_b) - (t_b d_a). \]

如果 \(\Delta>0\),则把 \((a,b)\) 互换能减少总花损(因为 \(t_b d_a < t_a d_b\)),如果 \(\Delta<0\) 则不必交换。如果 \(\Delta=0\) 则两种顺序等价。
因此,任一最优序列中不能存在使 \(\Delta>0\) 的相邻逆序对,也就是在最优序列中必有对于任意相邻的 \(a\)(前)和 \(b\)(后)满足

\[t_a d_b \le t_b d_a, \]

\(a\preceq b\)
由相邻交换可消去任何逆序,对所有对都满足 \(t_i d_j \le t_j d_i\)(即按关系 \(\prec\) 非递减排列)的序列是局部无逆序的,从而是全局最优的。

C++ 代码

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;

int n;
struct Node {
    int t, d;
} a[N];

bool cmp(Node a, Node b) {
    return a.t * b.d < b.t * a.d;
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &a[i].t, &a[i].d);
    sort(a + 1, a + n + 1, cmp);
    long long res = 0, t = 0;
    for (int i = 1; i + 1 <= n; i++) {
        t += a[i].t;
        res += (long long) 2 * t * a[i + 1].d;
    }
    printf("%lld\n", res);
    return 0;
}
posted @ 2025-11-11 15:50  Cocoicobird  阅读(0)  评论(0)    收藏  举报