训练赛7/9,ABCE题解

A - 涂色游戏

其实题目的意思就是有一个n*m的矩阵,每次我可以对一行或一列将其涂成k的颜色,最后输出这个矩阵每个格子是什么颜色就行。最开始时间复杂度算错了,以为暴力能直接过去,后面发现超时了,于是用了一种时间轴的方式写。

这个问题的核心是要模拟一个涂色游戏,在一个 n x m 的网格上进行 q 次操作,每次操作要么涂某一行,要么涂某一列。在每次操作之后,某个单元格的颜色由最后一次影响它的操作决定。我们需要在操作完成后输出整个网格的最终状态。

以下步骤:

  • 数据结构设计
    • 使用两个数组 anscnt 分别记录每一行和每一列的最新涂色操作。每个数组元素是一个结构体 node,包含颜色 (c) 和时间戳 (t)。
  • 处理输入和初始化
    • 读取网格的行数 n、列数 m 和操作数 q
    • 初始化 anscnt 数组,将所有颜色初始化为 0(白色),时间戳初始化为 0。
  • 处理每次操作
    • 依次读取每个操作,根据操作类型(涂行或涂列)更新相应行或列的颜色和时间戳。时间戳使用递增的 temp 变量来确保每次操作都有一个唯一的时间戳。
  • 生成最终网格
    • 遍历每个单元格,通过比较其所属行和列的时间戳来确定最终颜色。哪个时间戳较大,就说明哪个操作是最后影响这个单元格的操作。

方法分析:

  • 唯一性和顺序性

    • 时间戳保证了操作的唯一顺序,每次操作都能记录其发生的准确时间。
    • 使用结构体 node 存储颜色和时间戳,使得我们可以方便地比较行和列的最后一次操作。
  • 效率

    • 每次操作仅仅是更新对应行或列的颜色和时间戳,时间复杂度为 O(1)。
    • 生成最终网格时,需要遍历整个网格,时间复杂度为 O(n * m),这是必要的,因为每个单元格的颜色都需要计算。

证明:

假设经过 q 次操作后,我们得到了两个数组 anscnt,其中 ans[i] 记录了最后一次对第 i 行的操作(包括颜色和时间戳),cnt[j] 记录了最后一次对第 j 列的操作。

对于任意一个单元格 (i, j)

  • 如果 ans[i].t > cnt[j].t,则说明最后一次影响这个单元格的操作是对第 i 行的操作,因此这个单元格的颜色应该是 ans[i].c
  • 反之,如果 ans[i].t <= cnt[j].t,则说明最后一次影响这个单元格的操作是对第 j 列的操作,因此这个单元格的颜色应该是 cnt[j].c

这种方法确保每个单元格的颜色由最后一次影响它的操作决定,因为我们使用了时间戳来记录操作的顺序。

#include <bits/stdc++.h>
#define simeple freopen("input", "r", stdin),freopen("output", "w", stdout);
#define fast ios::sync_with_stdio(false),cin.tie(0);cout.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int MAX = 1e5 + 5;

struct node {
    int c, t;  // 颜色和时间轴
} ans[MAX], cnt[MAX];

int main() {
    fast
    //simeple
    int t;
    cin >> t;
    while (t--) {
        int n, m, q;
        cin >> n >> m >> q;
        // 初始化行和列的颜色和时间轴
        for (int i = 1; i <= n; i++) {
            ans[i] = {0, 0};  // 所有行初始化为颜色 0,时间轴 0
        }
        for (int i = 1; i <= m; i++) {
            cnt[i] = {0, 0};  // 所有列初始化为颜色 0,时间轴 0
        }
        int temp = 1;  // 初始化时间轴
        while (q--) {
            int opt, x, c;
            cin >> opt >> x >> c;

            if (!opt) {
                // 如果 opt 为 0,涂第 x 行颜色 c
                ans[x].c = c;
                ans[x].t = temp++;
            } else {
                // 如果 opt 为 1,涂第 x 列颜色 c
                cnt[x].c = c;
                cnt[x].t = temp++;
            }
        }
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (ans[i].t > cnt[j].t) {
                    cout << ans[i].c << ' ';
                } else {
                    cout << cnt[j].c << ' ';
                }
            }
            cout << endl;
        }
    }
    return 0;
}

B - 切绳子

意思就是有n条绳子,然后要切k份一样的,然后输出每条绳子切多少可以完成k份一样的。

这个问题的核心是要找到最大长度 L,使得从 N 条绳子中可以切出至少 K 条长度为 L 的绳子。可以利用二分查找,准确说应该是浮点数二分。

以下为步骤:

  • 初始化和输入处理

    • 读取输入的绳子数量 n 和目标条数 k
    • 读取每条绳子的长度,并记录其中的最大值作为初始的右边界 r
  • 二分查找

    • 初始化左边界 l 为 0。
    • 重复执行二分查找 100 次(精度足够)。
    • 在每次迭代中,计算中间值 mid,并检查能否从所有绳子中切出至少 k 条长度为 mid 的绳子。
    • 如果可以,则更新左边界 lmid,否则更新右边界 rmid
  • 结果输出

    • 将最终的 l 保留到小数点后 2 位(直接舍掉第 3 位后的小数)。注:要注意精度,一开始我没注意这一点WA了一发。
    • 使用 setiosflagssetprecision 设置输出格式

方法分析:

  1. 二分查找的有效性

    • 二分查找用于在一个已知范围内逐步缩小目标值。我们知道最大长度不会超过最初所有绳子的最大长度,因此以此为右边界。
    • 每次迭代都能有效地减少搜索空间,直到找到精确值。
  2. 验证函数 solove

    • 这个函数用于检查是否可以从所有绳子中切出至少 k 条长度为 mid 的绳子。通过将每条绳子的长度除以 mid 并求和,判断是否满足条件。

#include <bits/stdc++.h>
#define simeple freopen("input", "r", stdin),freopen("output", "w", stdout);
#define fast ios::sync_with_stdio(false),cin.tie(0);cout.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int MAX = 1e5 + 5;
int n, k;
double l = 0, r = 0;
vector<double> ans(1e5 + 5);

// 判断是否可以切出至少 k 条长度为 mid 的绳子
bool solove(double mid) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += ans[i] / mid;
    }
    return sum >= k;
}

int main() {
    fast
    //simeple
    cin >> n >> k;
    for (int i = 0; i < n; i++) {
        cin >> ans[i];
        r = max(r, ans[i]);  // 记录最大长度作为右边界
    }

    // 二分查找 100 次,精度足够
    for (int i = 0; i < 100; i++) {
        double mid = l + (r - l) / 2;
        if (solove(mid)) {
            l = mid;
        } else {
            r = mid;
        }
    }
    // 保留小数点后 2 位,直接舍掉第 3 位后的小数不这样做的话会报错,因为精度原因
    l = floor(l * 100) / 100;
    cout << setiosflags(ios::fixed) << setprecision(2) << l << endl;
    return 0;
}

C - 四方定理

题意就是一个数可以由四个整数的平方和得到,然后我们要计算出这个数可以有多少种不同组合。

这个问题是基于四平方和定理(Lagrange's Four Square Theorem),该定理指出每个正整数可以表示为最多四个整数的平方和。我们需要计算给定整数 n 可以分解为不超过四个整数平方和的方案总数。

其实是一个动态规划的完全背包问题,因为每个数字我们可以重复利用。

以下为步骤:

  • 定义状态

    • 使用一个二维数组 ans[j][k] 表示整数 j 可以分解成 k 个整数平方和的方案总数。
  • 状态转移

    • 对于每个可能的平方数 i*i,更新所有大于等于 i*i 的整数 j 的方案数。
    • 对于每个可能的分解数 k,通过 ans[j-i*i][k-1] 更新 ans[j][k]
  • 初始化和输入处理

    • 初始化 ans[0][0] = 1 表示 0 可以分解成 0 个整数平方和的方案数为 1。
    • 预处理所有可能的 jk 以便快速回答多个查询。

方法分析:

  • 动态规划的有效性

    • 动态规划方法通过将问题分解成子问题来解决,可以避免重复计算。
    • 通过迭代更新 ans 数组,确保每个状态都被正确计算。
  • 空间和时间复杂度

    • 由于 n 的最大值是 32768,因此数组 ans 大小为 (32768 + 3) x 5,空间复杂度可以接受。
    • 预处理时间复杂度为 O(n * sqrt(n) * 4),可以在合理时间内完成。

#include <bits/stdc++.h>
#define simeple freopen("input", "r", stdin),freopen("output", "w", stdout);
#define fast ios::sync_with_stdio(false),cin.tie(0);cout.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

int n, k;
double l, r;
vector<vector<int>> ans(32768 + 5, vector<int>(5, 0));

int main() {
    fast
    //simeple

    int i, j, l, n = 32768 + 3, t, cnt;
    cin >> t;
    ans[0][0] = 1;  // 初始化 0 可以表示为 0 个整数的平方和,方案数为 1
    // 预处理所有可能的平方数分解方案
    for (int i = 1; i * i <= n; i++) {
        for (int j = i * i; j <= n; j++) {
            for (int k = 1; k < 5; k++) {
                ans[j][k] = ans[j][k] + ans[j - i * i][k - 1];
            }
        }
    }
    while (t--) {
        cnt = 0;
        cin >> n;
        for (i = 1; i < 5; i++) {
            cnt = cnt + ans[n][i];
        }
        cout << cnt << endl;
    }
    return 0;
}

E - 切蛋糕

这题意思就是有n块蛋糕,我最多可以连续吃m块,每块蛋糕代表一个数字,我怎样可以使我的数子最大,我第一眼看这题跟我之前写的一道子序列的类似,算时间复杂的算错了,以为暴力可以,后面发现就两个例子卡了,然后想着优化过去,结果还是超时,然后挨了6发老实了(已老实求放过表情包,假装有图),于是用前缀和、双端队列过去了。

这个问题是一个典型的滑动窗口(双端队列)问题,目标是在一个数列中找到长度不超过 m 的子段,使得这个子段的元素和最大。

以下为步骤:

  • 前缀和数组

    • 使用前缀和数组 ans 使得每个位置 i 的值 ans[i] 表示从第 1 个元素到第 i 个元素的总和。
    • 通过前缀和数组,可以在常数时间内计算任意子段的和。
  • 双端队列

    • 使用双端队列 que 来维护当前窗口的最优起点(使得子段和最大)。
    • 双端队列的头部(hh)存储当前子段的起点,尾部(tt)存储当前子段的终点。
  • 窗口更新和维护

    • 遍历每个元素,计算以当前元素为终点的子段和,通过与前缀和队列头部元素做差得到子段和。
    • 使用双端队列来维护窗口的最优起点,并确保队列中的元素是单调递增的。
    • 确保队列的长度不超过 m

方法分析:

  • 前缀和数组的使用

    • 首先,我们使用一个前缀和数组 ans,其中 ans[i] 表示数组中前 iii 个元素的累积和。构建这个数组的时间复杂度是 O(n),因为我们只需遍历一次数组。
  • 双端队列的使用

    • 我们使用一个双端队列 que 来维护当前窗口内的最优解。队列中存储的是前缀和数组 ans 的索引。
    • 遍历数组时,对于每个位置 i,我们都要维护队列的单调性。具体来说:
      • 从队列尾部开始,将小于等于 ans[i] 的元素移除,以保持队列中的元素按照从小到大的顺序排列。
      • 这个操作的时间复杂度是 O(1) 次,因为每个元素最多被插入和删除一次。
  • 窗口的维护

    • 对于每个位置 i,计算当前位置与队列头部位置的前缀和之差,即可得到当前滑动窗口的子段和。这个操作的时间复杂度也是 O(1)。
    • 如果队列长度超过了 m,我们从队列头部移除元素,以保持窗口长度不超过 m。这个操作也是 O(1) 时间复杂度内完成的。
  • 遍历和结果输出

    • 整个遍历过程是 O(n),因为我们只对数组进行了一次完整的遍历。
    • 最后,输出最大子段和的时间复杂度也是 O(1)。

#include <bits/stdc++.h>
#define simeple freopen("input", "r", stdin),freopen("output", "w", stdout);
#define fast ios::sync_with_stdio(false),cin.tie(0);cout.tie(0);
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

int main() {
    fast
    //simeple
    int n, m, hh = 0, tt = -1;
    cin >> n >> m;
    vector<int> ans(5e5 + 10);
    vector<int> que(5e5 + 10);
    
    for (int i = 1; i <= n; i++) {
        cin >> ans[i];
        ans[i] = ans[i] + ans[i - 1];  // 构造前缀和数组
    }
    
    que[++tt] = 0;  // 初始时队列中只有前缀和的起点
    int cnt = INT_MIN;  // 初始化最大子段和为最小值
    
    for (int i = 1; i <= n; i++) {
        int temp = ans[i] - ans[que[hh]];  // 计算当前子段和
        cnt = max(cnt, temp);  // 更新最大子段和
        // 维护单调队列,确保队列中的前缀和单调递增
        while (tt >= hh && ans[que[tt]] >= ans[i]) {
            tt--;
        }
        que[++tt] = i;
        // 确保队列长度不超过 m
        if (i - que[hh] + 1 > m) {
            hh++;
        }
    }
    cout << cnt;
    return 0;
}

  • 49
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值