2024CSP-J题解

第一题跟第二题很简单,代码量也不大(好实现),因此本文主要讲解第三题和第四题。

第三题

简略讲一下思路吧。

其实就是一个小贪心+模拟。

首先题目要求我们用n个小木棍去拼凑出尽可能小的数(不含前导0)。

这个时候就要想,我们在比较两个数之间的大小关系时,先看什么东西。

思考ing......

不含前导0!!!

对于两个数之间大小关系的比较,我们通常先看数的位数,位数多的那个数就一定比较大,因此我们如果要拼凑出尽可能小的数,首先需要最小化数的位数

最小化数的位数——那么对于每个位,我们需要用掉尽可能多的小木棍(贪心)

显然,数字 '8'是消耗最多木棍的数,因此,我们先把所有木棍都去拼数字‘8’

这个时候就会产生一个新的问题:小木棍有剩余。(剩余木棍的数量 < 7)

对于这些剩余木棍,我们需要将前面的‘8’拆分一下,然后再去拼凑出数字。

对于拼凑数字的情况就非常暴力了。自己打一下表即可。

第四题

动态规划+优化

首先注意到题目给定两个条件:

1、每个人选择子数组的长度为[2, k],以及接龙规则。

2、同一个人不能连续两个回合出现,假设现在是第i个回合,上一回合(i - 1个回合)用的A, 那么当前(第 i 个)回合就不能用A了,但是在下一个回合(第 i + 1个回合)就能再次使用A。

然后是求在进行 Ri 回合之后得到的字符串能否以 Ci 结尾。

显然是DP了对吧

那么DP属性怎么选呢?

注意到题目要我们求的是 Ri 回合之后得到的字符串能否以 Ci 结尾。因此DP数组的属性为bool值

选好了DP数组的属性之后,我们要考虑DP的维度,用多少维合适呢?一维?二维?三维?

首先需要记录一下第几轮,比如第 i 轮需要从第 i - 1轮转移,这需要一维

这个时候就需要结合一开始提到的两个条件了,首先需要满足接龙条件——上回合的末尾字符与当前回合的首字符相等——保存上一轮的最后一个字符, 这需要一维

最后,需要维护上一回合进行操作的是哪个人(连续两个回合的人不能相同), 这同样需要一维

综上,初步分析DP数组需要三维,规模是R * n * A, R为轮数,n为人数,A为最大的数字

显然是不行的,因此进行本题的第二步——DP优化。

首先R的范围是100, n的范围是100000,A的范围是200000

因此,必须优化掉 n 或者 A。

这个时候我们不确定优化掉哪个,因此我们可以都试一试。

首先尝试优化掉A,那么显然是不可行的,因为我们的转移必须满足接龙规则,即我们在转移的时候需要枚举上一个数字是什么。

其次是优化n,现在的DP状态为f[r][v],表示进行r轮操作,以v为结尾是否是可行的。

那么这个时候状态当中就缺少了上一个回合是哪个人这个信息,现在考虑怎么去维护需要上一个回合是哪个人。注意到,对于一个状态f[r - 1][v],假设f[r - 1][v]可以通过A和B进行转移,即在第r - 1回合选择选手A和B都是合法的。那么对于当前枚举的这个人C来说,是不是只要找一个f[r - 1][c]除了通过自己进行转移的合法状态进行转移即可。观察一下可以发现,对于每个状态f[r - 1][v],我们只需要维护两个可以转移的人即可, 即f[r - 1][v] = {A, B}。那么只要在集合{A, B}中存在一个元素与当前枚举的C选手不同即可进行合法转移(连续两个回合的选手不同)。规模是R * A!!!

至此,这道题的思路就已经结束,实现上可能与上述有些许不同,见代码注释

#include<bits/stdc++.h>

using namespace std;

void solve(){
    int n, k, q;
    int mxr = -1, mxc = -1;
    cin >> n >> k >> q;
    vector<int>L(n + 1);
    vector<vector<int>>S(n + 1);
    for(int i = 1; i <= n; i++){
        cin >> L[i];
        S[i].resize(L[i]);
        for(int j = 0; j < L[i]; j++){
            cin >> S[i][j];
            mxc = max(mxc, S[i][j]);
        }
    }
    //cin >> q;
    vector<pair<int,int>>ask(q);
    
    for(int i = 0; i < q; i++){
        cin >> ask[i].first >> ask[i].second;
        mxr = max(mxr, ask[i].first);
    }
    //f[i][j] 表示i轮,以j结尾的情况是否能够被构造出来
    vector<vector<int>>f(mxr + 1, vector<int>(mxc + 1, -1));
    //初始状态,必须从1开始,那么第0次操作(实际上是不存在的)的结尾就是 1
    f[0][1] = 0;

    //1、如何判断上一轮是哪个人进行操作,通过f[i][j]存储的值进行判断
    //-1 表示不能完成任务, num && num != 0表示上一次操作的人是num, num = 0 表示上一次操作的人可以是{p1, p2, p3, ...}
    
    //2、如何判断每个人的序列长度
    //所谓的单调队列优化!!!
    //比如...
    //假设j > i, 能够从j转移就不必从i转移
    
    //枚举轮数
    for(int i = 1; i <= mxr; i++){
        //枚举每个人
        for(int j = 1; j <= n; j++){
            //-1表示上一个不能转移
            int lst = -1;
            for(int p = 0; p < L[j]; p++){
                int v = S[j][p];
                //最近能转移的点是lst,如果这个距离大于k,那么表示不能转移
                if(p - lst >= k){
                    //第j个人不能选择子数组[l, p],因为长度大于k
                    lst = -1;
                }
                if(lst != -1){
                    //更进行本轮循环的转移,操作之后自然是从 i - 1 --> i
                    if(f[i][v] == -1){
                        f[i][v] = j;
                    }

                    //多于两个状态能进行转移
                    else if(f[i][v] != j)f[i][v] = 0;
                }
                //更新最右能转移的端点
                //注意,这里是更新的左端点,因此i - 1轮的最后一个值应该是v
                if(f[i - 1][v] != -1 && f[i - 1][v] != j)lst = p;
            }
        }
    }

    for(auto &[r, c] : ask){
        if(c > mxc)
            cout << 0 << endl;
        else if(f[r][c] != -1)
            cout << 1 << endl;
        else
            cout << 0 << endl;
    }
    return;
}

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

为了对萌新友好,在这里解释一下类似单调队列的转移

因为在上面的代码中内层的两重循环相当于是在枚举右端点,那么这个时候我们就需要在右端点选择S[j][p]的前提下去看有没有合法的左端点(距离当前下标p的距离不超过k都可以转移),那么我们可以在尝试枚举左端点O(k),去枚举哪些距离下标p不超过k的左端点。

for(int i = 1; i <= mxr; i++){
    //枚举每个人
    for(int j = 1; j <= n; j++){
        //-1表示上一个不能转移
        int lst = -1;
        for(int p = 0; p < L[j]; p++){
            for(int left = p - 1; p - left < k; left--){
                //代码逻辑
            }
        }
    }
}

注意!!!这会超时,因为k是200000,这个时候我们只需要维护最大的合法转移下标lst,在转移的时候通过lst进行转移即可。

本文到此结束。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值