Google Kickstart 2019 Round B 题解

比赛题目

1. Building Palindromes

Problem:
有N个编号从1到N的block,每个由字母A到Z组成,对N个blocks进行Q次查询,每次查询给出block编号的左右边界Li和Ri,判断每次能否对该范围的blocks重新排序使其组成回文串。
Solution:

  1. 最简单直观的想法是对每次查询进行暴力判断,即为每次查询区间新建一个map存储不同字母出现的频次,如果区间长度为奇数,那么出现一次的字母恰好一个,如果区间长度为偶数,那么应该不存在仅出现一次的字母。实现代码如下,注意区间开闭。单次查询O(n),总时间复杂度O(N*Q)。
int n, q;
string s;
bool solve(int x, int y) {
    int len = y - x;
    if (len == 1) 
        return true;
    if (len == 2) 
        return (s[x] == s[x + 1]);
    map m;
    for (int j = x; j < y; ++j) {
        m[s[j]]++;
    }
    map::iterator it;
    int cnt = 0;
    for (it = m.begin(); it != m.end(); ++it) {
        if ((it->second) & 1) 
            cnt++;
    }
    if (len % 2 == 0) 
        return (cnt == 0);
    else 
        return (cnt == 1);
}
  1. 对上述代码进行优化,我们需要加快每个字母频次的计算,如果我们对整个字母串每个字母的出现频次计算前缀和,这样每次子串的各个字母出现频次查询只需要O(1)的时间。建立所有字母前缀和的时间为O(N * |character set|),每次区间查询用时O(|character set|),总的时间复杂度O((N+Q) * |character set|)。

对处理区间问题常用的前缀和、差分、树状数组、线段树相关知识的总结参考区间问题。

int n, q;
string s;
int a[100002][26];

void preprocess() {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < 26; ++j) {
            a[i + 1][j] = a[i][j];
        } 
        a[i + 1][s[i] - 'A']++;
    }
}
bool solve(int x, int y) {
    int len = y - x;
    int cnt = 0;
    for (int i = 0; i < 26; ++i) {
        if ((a[y][i] - a[x][i]) & 1)
            cnt++;
    }
    if (len % 2 == 0) {
        return (cnt == 0);
    }
    else {
        return (cnt == 1);
    }
}
int main() {
    int T;
    ios::sync_with_stdio(false);
    cin >> T;
    for (int t = 1; t <= T; ++t) {
        cin >> n >> q;
        cin >> s;
        preprocess();
        int res = 0;
        for (int i = 0; i < q; ++i) {
            int x, y;
            cin >> x >> y;
            if (solve(x - 1, y))
                res++;
        }
        cout << "Case #" << t << ": " << res << endl;
    }
}

根据回文的奇偶数规律上面根据len长度进行返回值的判断也可直接写作

    if (cnt <= 1)
        return true;
    else
        return false;

2. Diverse Subarray

Problem:
给定N件饰品,每件饰品的类型为Ai,现在需要选择某个连续区间内的饰品带走,当选择区间[l, r]内的饰品时如果某种类型的饰品的个数多于S个则该类饰品必须全部被丢掉。任意选择l和r,求能够带走的饰品的最大数量。
Solution:

  1. 最简单直观的想法仍然是对每个区间暴力判断,每个区间新建map记录每类饰品的个数,根据规则计算能带走的饰品的最大数目,更新结果。注意下面代码的优化,在第二轮循环中我们从后向前依次判断,当区间长度小于当前结果数时直接剪枝,对于T <= 100, N <= 1000的test set 1可过。
int n, s;
int a[100001];

int func(int x, int y) {
    map m;
    for (int i = x; i <= y; i++) {
        m[a[i]]++;
    }
    int res = y - x + 1;
    map::iterator it;
    for (it = m.begin(); it != m.end(); it++) {
        if ((it->second) > s) {
            res -= (it->second);
        }
    }
    return res;
}

int solve() {
    int res = 1;
    for (int i = 0; i < n - 1; i++) {
        for (int j = n - 1; j > 0; j--) {
            if((j - i + 1) < res)
                break;
            int tmp = func(i, j);
            res = max(res, tmp);
        }
    }
    return res;
}
  1. 设计更优的算法,我们首先固定区间起始点,试图在O(n)时间内计算出当前可能区间的最大可带饰品数,那么我们不能再使用map统计每个类型出现的次数,因为这样丢失了位置信息。我们可以将原始序列转换为基于类型判断的加减差分序列,这样问题转换为求每个固定起始点的转换序列的最大连续和,时间复杂度O(n^2)。
    例如,

    起始点为1 可构造
    , 最大连续和为3
    起始点为2 可构造
    ,最大连续和为4
    如果在-2后仍出现相同类型,转换为+0即可,在上述起始点后移的过程中我们发现只需要将1对应的类型中的-2改为+1, 后续的第一个+0改为-2即可。这样我们对区间起始点遍历O(n)的时间也可以进行优化,只需使用满足两类操作的数据结构:
    支持单点修改,支持区间求和
    因此,我们可以使用树状数组或线段树来实现。

你可能感兴趣的:(Google Kickstart 2019 Round B 题解)