寒假思维训练day18 D. Boris and His Amazing Haircut

今天更新一道1700的构造。


寒假思维训练day18


摘要

Part1  题意, 链接(有需自取,Problem - 1779D - Codeforces)

Part2  题解

Part3  代码(C++代码)

Part4  每日回顾一个基础算法|数据结构计划(今日:树状数组)

Part5  对构造题尝试总结(附带例题)


Part1  题意

题意:
给定长度为n的数组a,b, 再给定一个长度为m的数组x,其中1\leqslant n,m\leqslant 2e5, 1\leqslant a[i], b[i], x[i] \leqslant 1e9,每次可以对数组a进行以下操作,使用数组x的一个元素(注意x的每个元素只能使用一次),再选择任意的l,r,使得i\epsilon [l, r], a[i]:=min(a[i], x),问能否使得a变成b,能输出"YES",否则输出"NO"。


Part2  题解

题解:
1、引出问题:我们考虑如何构造一个方案使得a经过这个方案操作后不存在冲突就是有解的,否则判断一定无解。

2、先根据b[i]的值进行分块,因为b[i]最大达到了1e9所以先对b[i]哈希,映射到0-2e5存在vector中,记录每个x[i]的数量为cnt[b[i]]

3、考虑一下从小到大贪心,每次先处理小的,我们可以发现这种情况下我们必须绕过比较大的,因为一旦将较大值改成较小值,往后无法再将其改回来。

4、此时我们不妨逆着思考,从大到小这样就可以边维护边去判断,每次我们就从大到小遍历b[i]值相同的块,例如当b[i] = v,序列为 \{i_1, i_2, i_3, ... i_k\}, 因为我们每次都是对区间操作,如果中间存在较大块我们应该绕过它,使得需要的刀数+1。

        4.1 怎么维护已经被处理过的较大块, 我们可以用树状数组进行标记,当处理完当前块时,我们让每个位置+1, 当我们需要知道某个区间是否存在已经处理的块需要跳过,我们直接O(log(n))的查询前缀和是否为0即可,遍历每个值进行标记最多也就是O(N * log(N))。具体代码如下:

for(int i = 0; i < us[t].size(); i ++ ) add(us[t][i], 1); 

        4.2 对于b[i]怎么得到最小需要刀数, 对于每个块我们最多是O(N)的去查询,每次我们可以从起点出发判断与下一个索引是否之间的树状数组标记是否是大于0的,大于0要继续分,为0可以合并下一个,还要特判一种情况,当满足a[idx] = b[idx]时,我们应该直接跳过,但是能合并可以直接合并,因为它等于我们的b[i]块,但是他不应该再多拿块,具体代码如下:

        

int c = 0;
for (int i = 0; i < us[t].size(); i++) {
    if (a[us[t][i]] == b[us[t][i]]) continue;
    int j = i;
    ++c;
    while (j + 1 < us[t].size() && sum(us[t][j + 1]) - sum(us[t][j]) == 0)
        ++j;
    i = j;
}

        4.3 该贪心策略的合理性,因为大值块不影响小值的块,且考虑了比当前较大的块的位置,所以一定没有冲突。

5、根据这个流程往下,边做边判断即可得到答案。
 


Part3 代码(C++代码)

#include  
#define int long long
#define lowbit(x) (x&-x) 
using namespace std; 
constexpr int N = 1e6 + 10; 
int n, m, idx; 
int a[N], b[N], rz[N]; 
int tr[N]; // 树状数组标记数组
// mp是将b[i]映射到0-2e5数字上面
// _mp是mp的反映射,"反函数"
// rz[i]是剃刀的长度数组
map mp, _mp, cnt;  

void add(int x, int c) {
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c; 
}

int sum(int x) {
    int res = 0; 
    for(int i = x; i; i -= lowbit(i)) res += tr[i]; 
    return res; 
}
// 按照b的大小进行排序,这样子可以从大到小直接遍历
bool cmp(int x, int y) {
    return _mp[x] > _mp[y]; 
}

void solve() {
    mp.clear(); 
    _mp.clear(); 
    cnt.clear(); 
    bool flag = 1; 
    idx = 0; 
    cin >> n; 
    for(int i = 0; i <= n ; i ++ ) tr[i] = 0; 
    for(int i = 1; i <= n; i ++ ) cin >> a[i]; 
    for(int i = 1; i <= n; i ++ ) {
        cin >> b[i];
        if(a[i] < b[i]) 
            flag = 0; 
    }

    cin >> m; 

    for(int i = 1; i <= m; i ++ ) {
        cin >> rz[i];
        cnt[rz[i]] ++; 
    }
    
    vector B; 
    for(int i = 1; i <= n; i ++ ) {
        if(!mp[b[i]]) {
            mp[b[i]] = ++ idx; 
            _mp[idx] = b[i]; 
        }
        if(a[i] != b[i] && !cnt[b[i]]) {
            flag = 0; 
            break; 
        }
    }
    if(!flag) {
        cout << "NO" << endl; 
        return; 
    }
    
    for(int i = 1; i <= idx; i ++ ) B.push_back(i); 
    sort(B.begin(), B.end(), cmp);
    
    vector us[idx + 2]; 
    for(int i = 1; i <= n; i ++ ) us[mp[b[i]]].push_back(i);

    for(auto t : B) {
        int c = 0; 
        for(int i = 0; i < us[t].size(); i ++ ) {
            if(a[us[t][i]] == b[us[t][i]]) continue; 
            int j = i;
            ++ c; 
            while(j + 1 < us[t].size() && sum(us[t][j + 1]) - sum(us[t][j]) == 0)
                ++ j; 
            i = j; 
        }
        if(cnt[_mp[t]] < c) {
            flag = 0; 
            break; 
        } 
        for(int i = 0; i < us[t].size(); i ++ ) add(us[t][i], 1); 
    }
    
    cout << (flag ? "YES" : "NO") << endl; 
}

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


Part4  树状数组回顾

默写回顾了一下板子。
1、树状数组的单点查询和单点修改模板:

int tr[N]; 

int lowbit(int x) 
{
    return (x & -x); 
}

// 在x位置上面增加c
void add(int x, int c) 
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c;  
}

// 求到x的前缀和
int sum(int x) 
{
    int res = 0; 
    for(int i = x; i; i -= lowbit(i)) 
        res += tr[i]; 
    return res; 
}

2、树状数组的区间修改和区间查询:
 

int tr1[N], tr2[N]; 

int lowbit(int x) 
{
    return (x & -x); 
}

void add(int tr[], int x, int c) 
{
    for(int i = x; i <= n; i += lowbit(i)) tr[i] += c; 
}

int sum(int tr[], int x) 
{
    int res = 0; 
    for(int i = x; i; i -= lowbit(i)) res += tr[i]; 
    return res; 
}

int get(int l) 
{
    return (l + 1) * sum(tr1, l) - sum(tr2, l);  
}
signed main() 
{
    scanf("%lld%lld", &n, &m); 
    
    for(int i = 1; i <= n; i ++ ) 
        scanf("%lld", &a[i]); 
    
    for(int i = 1; i <= n; i ++ ) 
        add(tr1, i, a[i] - a[i - 1]), add(tr2, i, i * (a[i] - a[i - 1])); 
        
    while(m --) 
    {
        char op[2]; 
        int l, r, d; 
        scanf("%s%lld%lld", op, &l, &r); 
        if(*op == 'C')  // 区间修改
        {
            scanf("%lld", &d); 
            add(tr1, l, d), add(tr1, r + 1, -d); 
            add(tr2, l, l * d), add(tr2, r + 1, -(r + 1) * d); 
        }
        else 
        {
            // 区间查询 
            printf("%lld\n", get(r) - get(l - 1)); 
        }
    }
    
    return 0; 
}

Part5   对构造题尝试总结(附带例题)

1、前后缀贪心,比如说观察前后缀的sum,去看以后怎么考虑最好。Problem - 1903C - Codeforces

2、双指针贪心法,考虑两端相消或者相互作用,还有就是考虑左右边界。   Problem - 1891C - Codeforces

Problem - 1907D - Codeforces

3、转换观察法,有些关系可以抽象成图,观察图的某些性质去总结规律。也可以抽象成一个集合,两个集合相等可以说明有解可构造。Problem - 1891C - Codeforces

4、打表找规律,一般没什么规律可循即可打表找规律,一般和数论有关的很喜欢考,acm也喜欢考,属于人类智慧题。Problem - 1916D - Codeforces

5、公式推导演算,常见的分为公式的等价变形、公式的化简(这个常考,一般需要先证明某些性质,可以直接抵消,一般如果原公式处理起来很复杂时就可以考虑)。Problem - 1889B - Codeforces

6、考虑奇偶数去简化问题或者分类问题,从其中的一些运算性质入手,因为奇数偶数的加减以及%运算(这个结论很重要)的结果的奇偶性是固定的,Problem - 1898C - Codeforces

7、根据性质构造模型,看看能不能分成几个块,几个不同的集合,再选择算法去解决。Problem - 1873G - Codeforces

8、考虑从小到大处理,或者是从大到小处理,有时候先处理小的对大的不会有影响,或者反过来,这样的处理顺序是最完美的。Problem - 1904D2 - Codeforces

9、边界贪心法,一般要在问题的最边界处考虑,有时候这样做结果是最优的,或者考虑边界上的影响,假如让影响最小,就使得影响<= 固定值 。 ​​​​​​Problem - E - Codeforces and Problem - 1903C - Codeforces

你可能感兴趣的:(算法,c++,c语言)