跳转至

CSP模拟赛day3

T1 山丘(mountain)

签到题。

将输入的字符画看作是 $n$ 个单独的字符串 $s$。遍历输入的字符串 $s$,若存在相邻两个字符 $s_i,s_{i+1}$,满足:s[i] = '/‘ && s[i+1] = '\\',则答案加 $1$ 即可。

代码
int ans = 0;
for (int i = 0; i < n; i++)
{
    string s;
    cin >> s;
    for (int j = 0; j + 1 < m; j++)
    {
        ans += (s[j] == '/' && s[j + 1] == '\\');
    }
}
cout << ans << '\n';

T2 字符对字符错(string)

知识点:枚举,计数。

答案可以通过总的字符对数量减去“字符对”的个数来求得。

总字符对数为:$\binom{n}{2}=\frac{n \times (n-1)}{2}$。

为了最大化“字符错”的数量,应该最小化“字符对”的数量。

如果整个字符串不存在 ?,则“字符对”的数量是唯一的。

  • 预处理每个字母的出现次数记作 cnt[i],其中 $i\in[0,25]$。
  • 枚举每个字母,记作 c
  • 每个字母的次数 cnt[c] 对“字符对”的贡献为 $\binom{cnt[c]}{2}=\frac{cnt[c] \times (cnt[c]-1)}{2}$。

现在有若干个 ? 需要替换为某个字母,需求仍然是最小化“字符对”的个数。

  • 假设只有一个 ?,则将该 ? 替换为出现次数最少的那个字母。
  • 假设有多个 ?,则将这些 ? 依次替换为出现次数最少的那个字母。
    • 注意这一步随着字母次数的更新,出现次数最少的字母或许会变化,因此需要动态维护。

? 的个数为 sum,则循环 sum 次,每次遍历 $[0,25]$ 对应的字母,找到次数最小的那个字母记作 c,执行 $cnt[c]\gets cnt[c]+1$ 即可。

最后重新枚举每个字母的出现次数,计算更新后的“字符对”的个数。用总字符对数减去“字符对”的个数得到答案。

时间复杂度:$O(26n)$。注意使用 $\text{long long}$。

代码
string s;
cin >> s;
int sum = 0, n = s.size();
vector<int> cnt(26);
for (char c : s) 
{
    // isalpha(c) 判断是否为字母
    if (isalpha(c)) cnt[c - 'A']++;
    else sum++;
}
while (sum--)
{
    int mn = 1e9, c = -1;
    for (int i = 0; i < 26; i++)
    {
        if (cnt[i] < mn)
        {
            mn = cnt[i];
            c = i;
        }
    }
    cnt[c]++;
}
int sum = 0;
for (int x : cnt) sum += x * (x - 1) / 2;
cout << n * (n - 1) / 2 - sum << "\n";

T3 旅行(travel)

知识点:动态规划

子任务 $1$:$p_1=1$

由于字符串长度 $n\leq 2000$。此时的意思是买单程票是最划算的,因此统计两个字符串有多少个字符 1 即可。

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


子任务 $2$:$p_{pair}=1$

此时显然购买组合票是最划算的,但并不像子任务 $1$ 直接统计 1 的数量那样简单。

值得注意的是:买了组合票以后,接下来几天有可能是不需要继续买票的。

具体实现:

  • 使用一个名为 covered 的数组,记录每一天是否被组合票覆盖。
  • 遍历两个字符串,只有当 a[i]= '1' || b[i] = '1' 并且 covered[i] = false 时,才需要购票。
    • 且由于购买了组合票的缘故,需要将接下来的几天标记为 true

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


子任务 $3$:$\forall i\in[1,n],a_i=b_i=1$

此时每一天都需要有票。可以使用枚举法解决。

枚举 $p_3,p_{5},p_{pair}$ 分别买了 $x,y,z$ 张。检验:

  • covered = 3 * x + 5 * y + 4 * z
  • 则需要补 $n-covered$ 张单人票。

通过枚举,计算出买票花费的最小值。

乍一看时间复杂度为 $O(n^3)$,但由于 $3,5,pair$ 票的上限远达不到 $n$,因此实际可以通过这一部分。


子任务 $4$:$\forall i\in[1,n],b_i=0$

此时不需要考虑第二个人,只需要考虑第一个人怎么买票花费最少。

这部分可以使用 DP 完成。

定义 $dp_i$ 表示前 $i$ 天计划完成后的最少花费,答案为 $dp_n$。

转移:

  • 若 $a_i=0$,则 $dp_i=dp_{i-1}$
  • 否则:
    • 买单程票:$dp_i=\min(dp_i,dp_{i-1}+p_1)$
    • 买 $3$ 日票:$dp_i=\min(dp_i,dp_{\max(0,i-3)}+p_3)$
    • 买 $5$ 日票:$dp_i=\min(dp_i,dp_{\max(0,i-5)}+p_5)$
    • 买组合票:$dp_i=\min(dp_i,dp_{\max(0,i-4)}+p_{pair})$

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


子任务 $5$

让我们思考更一般的模式,不难想到:

定义 $dp_{i,j}$ 表示翁老师前 $i$ 天计划完成后以及聪聪老师前 $j$ 天的计划完成后的最少花费,答案为 $dp_{n,n}$。

初始化:

  • $dp_{0,0}=0$。
  • $dp_{i,0}=\min(dp_{i-1,0}+p_1\cdot [a_i=1],dp_{\max(0,i-3),0}+p_3,dp_{\max(0,i-5),0}+p_5,dp_{\max(0,i-4),0}+p_{pair})$。
  • 同理初始化:$dp_{0,i}$
  • 其余 $dp_{i,j}=\infty$。

接下来状态转移类似于子任务 $4$ 的状态转移。

  • $dp_{i,j}=\min(dp_{i-1,j}+p_1\cdot [a_i=1],dp_{i,j-1}+p_1\cdot [b_j=1])$
  • $dp_{i,j}=\min(dp_{\max(0,i-3),j}+p_3,dp_{i,\max(0,j-3)}+p_3)$
  • $dp_{i,j}=\min(dp_{\max(0,i-5),j}+p_5,dp_{i,\max(0,j-5)}+p_5)$
  • 注意:当 $i=j$ 时,才可以组合票,因为组合票是一起用的。
    • $dp_{i,j}=\min(dp_{i,j},dp_{\max(0,i-4),\max(0,j-4)}+p_{pair})$

注意初始化,注意 long long 等细节即可。


T4 冰淇淋(ice)

知识点:二分,枚举,博弈

子任务1

由于 $x=1,y=1$ 因此前两个选择都以固定,第三个选择是翁老师选择,翁老师的目的是最大化得分。

因此答案就是找到一个 $i$,使得 $|a_1+b_1+c_i-p|$ 最大即可。

直接枚举即可。


子任务2

仅仅 $x=1$,第一个选择已固定。第二个选择希望答案最小,第三个选择希望答案最大。

因此答案就是找到一个 $i,j$ 使得 $\max(|a_1+b_i+c_j-p|)$ 最小。

long long mn = 1e18;
for (int i = 1; i <= y; i++)
{
    long long mx = 0;
    for (int j = 1; j <= z; j++)
    {
        mx = max(mx, abs(a[1] + b[i] + c[j] - p));
    }
    mn = min(mn, mx);
}

子任务3

相比子任务 $2$ 多了一个第一层的选择,第一层的目的是希望答案最大,外面套一个求最大值的循环即可。

int ans = 0;
for (int i = 1; i <= x; i++)
{
    int mn = 1e18;
    for (int j = 1; j <= y; j++)
    {
        int mx = 0;
        for (int k = 1; k <= z; k++)
        {
            mx = max(mx, abs(a[i] + b[j] + c[k] - p));
        }
        mn = min(mn, mx);
    }
    ans = max(ans, mn);
}
cout << ans;

子任务4

注意到一旦 $i,j$ 确定后, $k$ 的目的就是让答案最大化。也就是 $|a_i+b_j-p|$ 已是定值记为 $S$,在找到一个 $c_k$ 使得 $|S+c_k|$ 最大。

不难证明:答案一定在 $\min(c_k)$ 和 $\max(c_k)$ 处取得。

因此不需要枚举 $k$,提前维护出 $\min(c_k)$ 和 $\max(c_k)$ 即可。

int ans = 0;
for (int i = 1; i <= x; i++)
{
    int mn = 1e18;
    for (int j = 1; j <= y; j++)
    {
        int mx = max(abs(a[i] + b[j] + mxc - p), abs(a[i] + b[j] + mnc - p));
        mn = min(mn, mx);
    }
    ans = max(ans, mn);
}
cout << ans;

子任务5

说白了就是不枚举 $j$ 也把答案求出来。

当枚举 $i$ 以后,设 $f(j)=\max(|a_i+b_j+\max(c_k)-p|,a_i+b_j+\min(c_k)-p)$。

定义 $L=a_i+\max(c_k)-p$,$R=a_i+\min(c_k)-p$。

则 $f(j)=\max(|L+b_j|,|R+b_j|)$。其中 $j\in[1, y]$,我们需要求出 $f(j)$ 函数的最小值,然后在针对枚举的每一个 $i$ 去求最大值即可。

子任务 $4$ 实际就是通过枚举 $j$ 找到了函数 $f(j)$ 的最小值而已。

如何快速找到函数 $f(j)$ 的最小值呢?

变形函数得到 $f(j)=\max(|b_j-(-L)|,|b_j-(-R)|)$。思考 $|a-b|$ 这个绝对值算式的几何意义:就是数轴上点 $a\to b$ 的距离。

因此 $f(j)$ 函数的几何意义就是 $b_j$ 离 $-L$ 和 $-R$ 的距离中较大的那个。

结论:

  • 要让它尽量小,就把 $j$ 放在两点的“切中位置”。
  • 直观证明:在中点处两项相等,此时最大者被“压平”到最小;一旦离开中点,远端那一项会变大,从而 $\max(\cdot)$ 增大。

具体实现

由于在实际题目中 $b_j$ 都是整数,因此求出 $\text{mid}=\lfloor \dfrac{-(L+R)}{2}\rfloor$。

使用 lower_bound 求出刚好大于等于 $\text{mid}$ 的 $j$。比较 $f(j),f(j-1)$ 二者哪个函数值最小,选最小的那个作为答案即可。

最终时间复杂度:$O(x\log{y})$。

int x, y, z, p, a[N], b[N], c[N];
int f(int L, int R, int j)
{
    return max(abs(L + b[j]), abs(R + b[j]));
}
signed main()
{
    cin >> x >> y >> z >> p;
    for (int i = 1; i <= x; i++)
        cin >> a[i];
    for (int i = 1; i <= y; i++)
        cin >> b[i];
    for (int i = 1; i <= z; i++)
        cin >> c[i];
    sort(b + 1, b + y + 1);
    sort(c + 1, c + z + 1);
    int mxc = c[z];
    int mnc = c[1];
    int ans = 0;
    for (int i = 1; i <= x; i++)
    {
        int L = a[i] - p + mxc, R = a[i] - p + mnc;
        int mid = -(L + R) / 2;
        int pos = lower_bound(b + 1, b + y + 1, mid) - b;
        int mn = 1e18;
        if (pos != y + 1)
        {
            mn = min(mn, f(L, R, pos));
        }
        if (pos != 1)
        {
            mn = min(mn, f(L, R, pos - 1));
        }
        ans = max(ans, mn);
    }
    cout << ans;
    return 0;
}