挑战赛 #15 题解
2025-02-24 08:59:17
发布于:浙江
T1
这是一道简单的字符串题目,任务是判断字符串中指定位置的字符,是否为给定的特定字符 。
    int n;
    string x;
    cin >> n >> x;
    if (x[n] == 'x')cout << "No" << endl;
    else cout << "Yes" << endl;
T2
判断两个字符串的字符集是否一样
在处理字符串问题时,我们常常需要判断两个字符串的字符集是否相同。有一种可行的方法是,分别统计这两个字符串中每个字符的出现情况,就像把它们各自的字符放入两个 “桶” 中一样。这里的 “桶” 可以理解为一种数据结构,用于记录每个字符的存在与否或者出现次数。最后,通过比较这两个 “桶” 里的数据,来确定这两个字符串的字符集是否一致。
    string x1, x2;
    cin >> x1 >> x2;
    map<char, int> mp1, mp2;
    for (auto i: x1) {
        mp1[i]++;
    }
    for (auto i: x2) {
        mp2[i]++;
    }
    for (auto [i,j]: mp1) {
        if (mp2[i] != j) {
            cout << "No" << endl;
            return;
        }
    }
    for (auto [i,j]: mp2) {
        if (mp1[i] != j) {
            cout << "No" << endl;
            return;
        }
    }
    cout << "Yes" << endl;
T3
针对这道题目,我们可以先进行一个关键的分析:鼹鼠的能力值越高,其能够放置的位置选择也就越多,适用范围越广。基于此特性,在处理时,我们可以采用贪心策略,即从前往后遍历每一个位置,在每个位置都尝试放置当前可放置的鼹鼠中能力值最小的那一只。
具体实现上,我们可以通过简单的搜索来完成。使用一个二维搜索方式,第一维用于标记我们想要到达的位置 i,第二维则是在尚未使用过的鼹鼠中寻找能力值最小的那一只。若能找到符合条件的鼹鼠,就表明我们可以顺利到达 i 位置;反之,若找不到,那就说明无法到达该位置。
需要特别注意的是,在使用完某只鼹鼠后,一定要给它打上已使用的标记,避免重复使用。
这种解题方法采用了贪心算法和顺序处理的思路,时间复杂度为 。
    int n;
    cin >> n; 
    vector<int> arr(n + 1);
    for (int i = 1; i <= n; i++) cin >> arr[i];
    vector<int> op(n + 1);
    for (int i = 1; i <= n; i++) cin >> op[i];
    vector<int> vis(n + 1);
    int now = 2;
    for (int i = 2; i <= n; i++) {
        int dis = arr[i] - arr[i - 1];
        int mins = 1e9 + 7;
        int len = 0;
        for (int j = 1; j <= n; j++) {
            //没用过  可用 更小花费
            if (!vis[j] && op[j] >= dis && mins > op[j]) {
                len = j;
                mins = op[j];
            }
        }
        if (len == 0) {
            cout << "NO" << endl;
            cout << i - 1 << endl;
            return;
        }
        vis[len] = 1;
    }
    cout << "YES" << endl;
接下来,我为大家介绍另一种解题方法。对于每个位置,我们的目标是找出所有鼹鼠中能够适用且能力值最小的那一只。为了高效地实现这一目标,我们只需维护一个特定的数据结构,该结构要具备快速查找所需数据的能力。
在具体的数据结构选择上,有两种不错的方案。其一,我们可以采用动态开点的权值线段树,并在其上进行二分查找操作;其二,也可以使用 set 容器,利用其内部自带的二分查找功能。
这种方法运用了二分查找的思想,时间复杂度为 ,相较于某些传统方法在效率上有显著提升。
    int n;
    cin >> n;
    vector<int> arr(n + 1);
    for (int i = 1; i <= n; i++) cin >> arr[i];
    set<pair<int, int> > s;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        s.emplace(x, i);
    }
    for (int i = 2; i <= n; i++) {
        int dis = arr[i] - arr[i - 1];
        auto it = s.upper_bound({dis, 0});
        if (it == s.end()) {
            cout << "NO" << endl;
            cout << i - 1 << endl;
            return;
        }
        s.erase(it);
    }
    cout << "YES" << endl;
T4
让我们来思考这样一个问题:给定 n 个数(其中 n 小于等于 20),对于每个数都有两种选择,即选或者不选。那么,总共会有多少种不同的选择情况呢?答案是 2 的 n 次方种,当 n 为 20 时,大约是 10 的 6 次方种情况,这个数量似乎还在可接受范围内。
然而,当 n 增加到 35 时,情况就截然不同了。此时,2 的 35 次方远大于 10 的 10 次方,这样的时间复杂度显然会导致程序运行效率极低,甚至可能超时。那么,我们该如何对这个问题进行时间复杂度上的优化呢?这里就要用到一个相对冷门但非常有效的知识点——折半搜索。
具体操作如下:首先,我们将这 n 个数分成前后两部分。对前半部分的所有数,穷举它们的所有选择情况,由于前半部分的数大约是 n/2 个,其组合情况数不到 10 的 6 次方种;后半部分也采用同样的方法进行处理。
现在问题来了,我们已经分别得到了前后两部分的所有组合情况,该如何将它们进行有效的组合呢?我们的目标是找出这些组合中对 x 取模结果最大的数。根据取模运算的性质,即 (x + y) % z 等于 (x % z + y % z) % z,我们可以先分别对前后两部分组合得到的数进行取模运算,然后再进行后续的计算,这样做并不会影响最终的结果。
同时,我们注意到一个重要的特性:对于前半部分集合中的任意元素 a,都有 a 小于 x;对于后半部分集合中的任意元素 b,也有 b 小于 x。由此可知,a + b 的值小于 2 * x。所以,我们只需要考虑两种情况:一是 a + b 小于 x 的情况,二是 x 小于等于 a + b 且 a + b 小于 2 * x 的情况。
针对这两种情况的处理,我们可以使用简单的双指针算法来高效实现。通过这种方式,我们可以在保证结果正确性的前提下,有效降低时间复杂度,避免因情况数过多而导致的效率问题。
    i64 n, x;
    cin >> n >> x;
    if (n == 1) {
        int t;
        cin >> t;
        cout << t % x << endl;
        return;
    }
    i64 l = n / 2;
    i64 r = n - l;
    vector<i64> vl(l);
    vector<i64> vr(r);
    for (auto &i: vl)cin >> i;
    for (auto &i: vr) cin >> i;
    auto get = [&](vector<i64> &vv, int nn) {
        set<i64> now;
        for (int i = 0; i < nn; i++) {
            set<i64> tmp;
            for (auto j: now) {
                tmp.emplace(j);
                tmp.emplace((j + vv[i]) % x);
            }
            tmp.emplace(vv[i] % x);
            now = tmp;
        }
        vector<i64> ans(now.begin(), now.end());
        return ans;
    };
    auto ll = get(vl, l);
    auto rr = get(vr, r);
    int mid1 = rr.size();
    mid1--;
    int last = mid1;
    i64 maxs = 0;
    for (long long i: ll) {
        while (mid1 > 0 && rr[mid1] + i >= x)mid1--;
        maxs = max({
            maxs,
            (i + rr[mid1]) % x,
            (i + rr[last]) % x,
            i
        });
    }
    cout << maxs << endl;
T5
我们留意到题目要求中必定存在一条从节点 i 到节点 i + 1 的路线。由此我们不禁思考,对于任意一条从节点 i 到节点 i + x(x > 1)的边,是否都属于无效边呢?是不是只需关注那些向前走的边就足够了?此外,如果我们已经到达了某个节点 i,那么是否意味着 i 之后的所有节点都能够到达呢?
如此一来,问题就转化为:在可以对边进行添加或删除操作的情况下,我们最终能够到达的最靠前的节点是哪一个?
针对这个问题,我们来分析有效边 (x, y) 所具备的性质。首先,显然需要满足 x > y,那么除此之外还有其他特性吗?仅观察一条边,似乎难以发现更多性质。那我们增加边的数量,假设有两条边 (r1, l1) 和 (r2, l2),我们可以将它们看作两条线段,端点分别为 l1、r1 和 l2、r2。
经过分析我们发现,若 l1 ≤ l2 ≤ r2 ≤ r1,那么线段 l2 r2 实际上是无用的;若 l1 < r1 < l2 < r2,这两条线段之间似乎没有明显的关联。然而,当出现 l1 ≤ l2 ≤ r1 ≤ r2 的情况时,对于处于区间 [l1, r2] 内的任意点 x,它可以先到达 r2,接着到 l2,再到 r1,最后到达 l1,这一过程似乎等价于存在一条从 (r2, l1) 的边。
换一种思路来理解,对于每一条区间为 [r, l] 的边,我们可以将其拆解为 r - l + 1 条长度为 1 的边。具体来说,这些边依次为 [r, r - 1]、[r - 1, r - 2]、……、[l + 1, l] 。通过这样的拆解方式,大家能更加直观、便捷地理解区间合并的相关知识 。
至此,这道题就变得清晰明了了。该问题可以转化为:给定一条线段,其上分布着若干子线段,并且允许对这些子线段进行添加或删除操作。现在有一些询问点 x,需要找出在区间 [1, x] 中最后一个没有被任何子线段覆盖的点。很明显,这是一个典型的扫描线问题。
这里给出一种使用支持区间加和线段树二分的解法。需要注意的是,这里采用的是线段树二分,而非二分线段树,因为二者的单次操作复杂度有所不同,前者为 ,而后者为 。
线段树模版大家可以自行学习,这里只给主函数代码
解法
    int n, q;
    cin >> n >> q;
    STG stg(n);
    for (int i = 1; i <= q; i++) {
        int op, x, y;
        cin >> op >> x;
        if (op == 1) {
            cin >> y;
            if (x > y) {
                stg.add(1, 1, n, y, x, 1);
            }
        } else if (op == 2) {
            cin >> y;
            if (x > y) {
                stg.add(1, 1, n, y, x, -1);
            }
        } else {
            int t = stg.biny_search(1, 1, n, 1, x);
            if (t == x) t--;
            cout << n - t << endl;
        }
    }
T6
题目要求十分清晰,给定两个字符串  和 ,需要找出字符串  的最长前缀 ,使得该前缀在字符串  中至少出现  次,判断这样的前缀是否存在,若存在则将其输出。
那么,该如何解决这个问题呢?下面为大家提供两种解法。
第一种解法,运用哈希字符串结合二分查找的方法。
哈希函数这个大家可以进行相关搜索,这里只给主函数
    string a, b;
    cin >> a >> b;
    int t;
    cin >> t;
    int now = 0;
    strhash::StringHash sh1(a), sh2(b);
    int n1 = a.length(), n2 = b.length();
    for (int i = 20; i >= 0; i--) {
        if (now + (1 << i) > n2 || now + (1 << i) > n1) continue;
        int tt = 0;
        int ll = now + (1 << i);
        for (int j = 0; j + ll <= n1; j++) {
            if (sh2.get(0, ll ) == sh1.get(j, j + ll )) {
                tt++;
            }
        }
        if (tt >= t) {
            now = ll;
        }
    }
第二种方法是利用 KMP 算法。在使用 KMP 算法进行字符串匹配时,模式串每移动一次都会产生一定的相对位移。此时,我们可以记录下模式串每次移动后的当前位置,并把这些位置信息存储到一个数据结构(可形象地将其视为“桶”)当中。
同样的模版大家自己学习去啦
    string a, b;
    cin >> a >> b;
    //计算a中每一个点匹配到b的哪个位置
    auto t = find_occurrences(a, b);
    //next函数
    auto z = prefix_function(b);
    // debug(z);
    int nn = b.size();
    vector<int> bn(nn + 1);
    for (auto i: t)bn[i]++;
    for (int i = nn; i; i--) {
        // debug(z[i- 1], i);
        bn[z[i - 1]] += bn[i];
    }
    int k;
    cin >> k;
    for (int i = nn; i; i--) {
        if (bn[i] > k) {
            cout << b.substr(0, i) <<endl;;
            return;
        }
    }
    cout << "IMPOSSIBLE" << endl;
全部评论 2
猖狂!
2025-08-20 来自 浙江
0%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2025-02-24 来自 广东
0虔诚拜三拜
2025-02-24 来自 广东
0
















有帮助,赞一个