跳转至

E. Blackslex and Girls

时间限制:每测试点 2 秒
内存限制:每测试点 256 兆字节

问题描述

在尝试使用固定长度的比特串的 De Bruijn 序列搭讪女孩失败后,Blackslex 将注意力转向了政治。

由于其高魅力,他现在负责为自己的国家的 n 个选区绘制边界。在 Blackslex 的国家,有 x 个支持 A 党的选民和 y 个支持 B 党的选民。凭借他惊人的绘图技巧,他可以将任何政党的选民分配到他选择的任何选区。

他与比特串的渊源让他思考,是否能够分配选民,使得每个选区的胜者遵循某种特定的比特串模式。为了避免引起怀疑,他还必须为每个选区分配至少 p_i 名选民。请告诉他这是否可能!

形式化地,给定一个长度为 n 的二进制字符串 s,一个长度为 n 的数组 p,以及两个整数 xy

你需要判断是否存在两个长度为 n 的非负整数数组 ab,满足以下条件: 1. a_1 + a_2 + \dots + a_n = x 2. b_1 + b_2 + \dots + b_n = y 3. 对于每个 1 \le i \le na_i + b_i \ge p_i 4. 对于每个 1 \le i \le n: - 如果 s_i = 0,则 a_i > b_i - 如果 s_i = 1,则 b_i > a_i

输入格式

第一行包含一个整数 t (1 \le t \le 10^4) —— 测试用例的数量。

每个测试用例的第一行包含三个整数 nxy (1 \le n \le 2 \cdot 10^5, 1 \le x, y \le 10^9)

第二行包含一个长度为 n 的二进制字符串 s

第三行包含 n 个整数 p_1, p_2, \dots, p_n (1 \le p_i \le 10^9)

保证所有测试用例的 n 之和不超过 2 \cdot 10^5

输出格式

对于每个测试用例,如果存在满足所有条件的数组 a, b,则输出 "YES"(不区分大小写),否则输出 "NO"

样例输入

6
3 5 5
010
2 4 3

4 2 3
0001
1 1 1 1

2 4 2
00
3 3
4 23 20
1111
2 2 2 2
1 25 26
0
51
2 4 2
00
3 4

样例输出

YES
NO
YES
NO
NO
NO

样例说明

在第一个测试用例中,一种可能的选民分配方案是:a = [2, 0, 3]b = [0, 4, 1]

在第三个测试用例中,一种可能的选民分配方案是:a = [2, 2]b = [1, 1]

对于其他测试用例,可以证明不存在满足条件的选民分配方案。

WA 2了

// Problem: CF 2179 E
// Contest: Codeforces - Codeforces Round 1071 (Div. 3)
// URL: https://codeforces.com/contest/2179/problem/E
// Memory Limit: 256 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define ios ios::sync_with_stdio(false);cin.tie(nullptr);
#define int long long

void solve(){
    int n,x,y;
    cin>>n>>x>>y;
    string s;
    cin>>s;
    vector<int> p(n);
    int na=0,nb=0;
    int flag1=0,flag2=0;
    for(int i=0;i<n;i++){
        cin>>p[i];
        if(s[i]=='0'){
            na=na+p[i]/2+1;
            nb=nb+(p[i]-p[i]/2-1);
            flag1=1;
        }else{
            nb+=(p[i]/2+1);
            na=na+(p[i]-p[i]/2-1);
            flag2=1;
        }
    }

    if(na>x||nb>y){
        printf("NO\n");
        return ;
    }

    if(flag1&&flag2){
        printf("YES\n");
    }else if(flag1&&flag2==0){
        if(x>=y+n) printf("YES\n");
        else printf("NO\n"); 
    }else{
        if(x+n<=y) printf("YES\n");
        else printf("NO\n"); 
    }


    return ;
}

signed main(){
    ios;
    int t;
    cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

AC 代码

// Problem: CF 2179 E
// Contest: Codeforces - Codeforces Round 1071 (Div. 3)
// URL: https://codeforces.com/contest/2179/problem/E
// Memory Limit: 256 MB
// Time Limit: 2000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;
#define int long long

void solve(){
    int n,x,y;
    cin>>n>>x>>y;
    string s;
    cin>>s;
    vector<int> p(n);
    int na=0,nb=0;
    int flag1=0,flag2=0;
    int tot=0;
    for(int i=0;i<n;i++){
        cin>>p[i];
        tot+=p[i];
        if(s[i]=='0'){
            na=na+p[i]/2+1;
            flag1=1;
        }else{
            nb+=(p[i]/2+1);
            flag2=1;
        }
    }

    if(na>x||nb>y){
        printf("NO\n");
        return ;
    }

    if(x+y<tot){
        printf("NO\n");
        return ;
    }
    if(flag1&&flag2==0&&x<y+n){
        printf("NO\n");
        return ;
    }
    if(flag2&&flag1==0&&x+n>y){
        printf("NO\n");
        return ;
    }
    printf("YES\n");
    return ;
}

signed main(){
    int t;
    cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

WA 2的原因

这是对之前错误的彻底修正

之前的核心误区在于:我这种“输家必须填坑”的假设是完全错误的。

1. 错误思路 vs 正确思路

  • ❌ 之前的错误思路(刚性填坑):

    • 我认为:如果选区 i 需要 10 票,赢家出了 6 票,那么输家必须出 4 票。

    • 导致的问题: 这强制要求输家手里必须有票。但实际上,如果赢家很有钱,赢家可以一个人出 10 票,输家出 0 票,这也是合法的(10 > 0 且 10+0 >= 10)。

  • ✅ 正确的思路(总量池子):

    • 赢家的责任是刚性的: 想要赢,必须出 \lfloor p_i/2 \rfloor + 1 票。这是门票,少一张都不行。

    • 输家的责任是弹性的: 输家不一定要出钱。

    • 剩余的票数(p_i 的缺口): 只要 xy总和足够大,能够填满所有选区的 p_i 需求即可。不管是 A 出还是 B 出,只要填满了 p_i 且不破坏赢家的地位就行。


2. 重新分析:三个独立判定条件

要把这道题做对,需要检查且仅需要检查以下三个维度的限制。只要满足,就是 YES,否则 NO

条件一:赢家付得起“门票费”吗?

这是最优先的。不管总票数多少,A 必须拿下所有 s_i=0 的局,B 必须拿下所有 s_i=1 的局。

  • Cost_A = \sum_{s_i=0} (\lfloor p_i/2 \rfloor + 1)

  • Cost_B = \sum_{s_i=1} (\lfloor p_i/2 \rfloor + 1)

  • 判定: 如果 x < Cost_A 或者 y < Cost_B,直接 NO

条件二:总票数够填满所有 p_i 吗?

赢家付完门票后,所有选区 i 还剩下 Rem_i = p_i - (\lfloor p_i/2 \rfloor + 1) 的票数缺口。这些缺口需要用 A 和 B 剩余的票来填。

实际上不需要算这么细,宏观来看:

  • 判定: 如果 x + y < \sum p_i,说明连最低总票数要求都达不到,直接 NO

条件三:防止“一边倒”的数学限制

这是唯一的特殊情况。

  • 如果 A 全赢(s 全是 '0'):

    • 因为每个选区都要求 a_i > b_i(即 a_i \ge b_i+1)。

    • 累加起来:\sum a_i \ge \sum b_i + n

    • x \ge y + n

    • 判定: 如果是全 0 且 x < y + n,直接 NO

  • 如果 B 全赢(s 全是 '1'):

    • 同理,要求 y \ge x + n

    • 判定: 如果是全 1 且 y < x + n,直接 NO

为什么“混色”的情况不需要判定?

如果既有 A 赢也有 B 赢,我们总能把多余的 A 票塞进 A 赢的区(扩大优势),把多余的 B 票塞进 B 赢的区(扩大优势),从而消耗掉所有多余票数而不破坏规则。只有在“一边倒”时,输家的票没地方扔,才会导致限制。


3. 正确逻辑的代码实现

这份代码完全基于上述三点逻辑,去掉了所有不必要的假设。

C++

#include <iostream>
#include <vector>
#include <string>
#include <numeric>

using namespace std;

typedef long long ll;

void solve() {
    int n;
    ll x, y;
    if (!(cin >> n >> x >> y)) return;

    string s;
    cin >> s;

    vector<ll> p(n);
    ll sum_p = 0;

    // 统计必须支出的“门票费”
    ll min_cost_a = 0;
    ll min_cost_b = 0;

    bool a_wins_something = false;
    bool b_wins_something = false;

    for (int i = 0; i < n; i++) {
        cin >> p[i];
        sum_p += p[i]; // 统计总需求

        ll win_cost = p[i] / 2 + 1;

        if (s[i] == '0') {
            min_cost_a += win_cost;
            a_wins_something = true;
        } else {
            min_cost_b += win_cost;
            b_wins_something = true;
        }
    }

    // 【判定 1】赢家必须付得起门票
    if (x < min_cost_a || y < min_cost_b) {
        cout << "NO\n";
        return;
    }

    // 【判定 2】总票数必须够填满所有需求
    if (x + y < sum_p) {
        cout << "NO\n";
        return;
    }

    // 【判定 3】一边倒的特殊限制
    // 如果全是 A 赢,A 的总票数必须比 B 多至少 n
    if (!b_wins_something && x < y + n) {
        cout << "NO\n";
        return;
    }
    // 如果全是 B 赢,B 的总票数必须比 A 多至少 n
    if (!a_wins_something && y < x + n) {
        cout << "NO\n";
        return;
    }

    // 如果通过了所有考验
    cout << "YES\n";
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int t;
    if (cin >> t) {
        while (t--) {
            solve();
        }
    }
    return 0;
}

总结错误的教训

这次的教训在于贪心策略的边界

  • 之前的错误:我把局部最优(让每个选区正好卡在 p_i)当成了全局限制。我担心“票浪费了怎么办”,所以强行让输家填坑。

  • 现在的修正:这道题的核心不在于“省票”,而在于“只要总票够,怎么分都行”。输家是没有义务出票的,只要总票数够填满 p_i,具体的分配可以非常灵活(比如赢家全包)。

现在这个逻辑是严丝合缝的。