2024.1.19训练赛总结

赛时做出 ABC

链接 https://vjudge.net/contest/604628

A题

思路:先处理要求相等的输入,将被要求相等的变量放在一个并查集中,然后对要求不相等的输入进行判断,如果要求 xi != xj,但 xi 和 xj 在同一个并查集中,则输出 NO,否则 YES

问题:

第一,要不是看了原题,我都不知道下标有1e11的范围,这个只需要简单离散化就可以

第二,数组又开小了,导致一直 wa 和 mle服了

#include<bits/stdc++.h>

const int MAXN = 1e5 + 10;
using namespace std;
typedef long long LL;

struct node{
    LL a,b;    
    int op;
    
};
node q[MAXN], ne[MAXN];
int T;
int n;
int cnt, cnt2;
LL tot[MAXN << 1];
int f[MAXN];
map<LL, int> mp;

int findfa(int x)
{
    if(x == f[x]) return f[x];
    return findfa(f[x]);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    cin>>T;
    while(T--)
    {
        cin>>n;
        mp.clear();
        cnt = 0;
        cnt2 = 0;
        for(int i = 1;i <= n;i++) 
            cin>> q[i].a >> q[i].b >> q[i].op, tot[++cnt2] = q[i].a, tot[++cnt2] = q[i].b;
            
        sort(tot + 1, tot + cnt2 + 1);
        tot[cnt2 + 1] = -1;
        
        for(int i = 1;i <= cnt2;i++)
            if(tot[i] != tot[i+1]) mp[tot[i]] = ++cnt;
        
        for(int i = 1;i <= cnt;i++) f[i] = i;
        cnt = 0;
        
        int op, l, r;
        for(int i = 1;i <= n;i++)
        {
            op = q[i].op;
            l = mp[q[i].a];
            r = mp[q[i].b];
            if(op)
            {
                int f1 = findfa(l), f2 = findfa(r);
                f[max(f2, f1)] = min(f2, f1);
            }
            else ne[++cnt].a = l, ne[cnt].b = r;
        }
        int flag = 1;
        for(int i = 1;i <= cnt;i++)
        {
            if(findfa(ne[i].a) == findfa(ne[i].b))
            {
                flag = 0;
                break;
            }
        }
        if(flag) cout<<"YES\n";
        else cout<<"NO\n";
    }
    return 0;
}
View Code

 

B题

思路:直接暴力模拟就可以了,直接用一个优先队列来存储区间,每次从队列里面取出一个最靠左,最长的区间,取中间赋值,再把两边的区间放到队列里,注意什么时候要放进队列,什么时候不需要放进队列就可以。比如出现了左边界大于右边界,边界超出了1和n的范围等

问题:

1.优先队列是大根堆,一直不记

2.在结构体中只能自定义小于号,不能大于号(存疑)

3.没注意边界问题,又wa了

#include<bits/stdc++.h>

const int MAXN = 2e5 + 10; 
using namespace std;

int T;
int n;
int ans[MAXN];
struct node{
    int l;
    int r;
    friend operator < (const node a,const node b){
        if(a.r - b.r == a.l - b.l) return a.l > b.l;
        return a.r - a.l < b.r - b.l;
    }
        
};
priority_queue <node> q;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    cin>>T;
    while(T--)
    {
        cin>>n;
        int cnt = 0;
        while(!q.empty()) q.pop();
        q.push({1,n});
        while(!q.empty())
        {
            node ne = q.top();
            q.pop();
            if(!ans[ne.l + ne.r >> 1]) ans[ne.l + ne.r >> 1] = ++cnt;
            if(ne.l >= ne.r || !ne.l || !ne.r || ne.l > n || ne.r > n) continue;
            q.push({ne.l, (ne.l + ne.r >> 1) - 1});
            q.push({(ne.l + ne.r >> 1) + 1, ne.r});
        }
        for(int i = 1;i <= n;i++) cout<<ans[i]<<" \n"[i==n],ans[i] = 0;
    }
    return 0;
}
View Code

 

C题

思路:一开始还没看出来是个经典线段树,后面才想明白。

每个节点除了保存边界,再多一个变量存储左右子树的 gcd 就可以了。

问题在于如何询问,即确定在所给区间内最多修改一个值,使得该区间的 gcd 恰好为 x。

做的时候的思路是:

求出来需要修改的次数,如果小于等于1,就 yes,否则 no

如果当前节点表示的区间是被包含在所给区间的,那就根据它的 gcd 值分以下几种情况来判断:

1.如果是 x 的倍数,说明不需要修改

2.如果不是 x 的倍数,如果是单个节点,修改次数+1,否则分别对它的两个子区间进行询问

关于为什么只要是 x 的倍数都不用修改,因为如果区间的 gcd 是 x 的倍数,那么只需要修改一次,随便把一个数修改成 x,最终就都满足了

而如果该区间有数不含 x 这个因子,或者两个或多个数的 gcd 不为 x,那就需要把它们全部修改成 x 了,所以有几个就得改几个,而且不需要修改那些 gcd 是 x 或 x 倍数的区间,因为一旦区间中有了 x,那整个区间的 gcd 一定不会超过 x。

问题:

如果不加优化,会有很明显的问题: 超时

这就需要剪枝了,只要当前需要修改的次数已经大于1,那就直接结束。(居然当时没想到)

总结:很好线段树,让我找回感觉,不再恐惧,第一次手写过。

#include<bits/stdc++.h>

const int MAXN = 5e5 + 10;
using namespace std;

int n;
int a[MAXN];
int q;
int op, poi, ll, rr, xx;
struct node{
    int l;
    int r;
    int g;
}t[MAXN<<3];

int gcd(int a,int b)
{
    if(!b) return a;
    return gcd(b, a % b);
}

void build(int rt, int L, int R)
{
    if(L == R)
    {
        t[rt].l = t[rt].r = L;
        t[rt].g = a[L];
        return;    
    }    
    int mid = L + R >> 1;
    build(rt << 1, L, mid);
    build(rt << 1 | 1, mid + 1, R);
    t[rt].l = L;
    t[rt].r = R;
    t[rt].g = gcd(t[rt << 1].g, t[rt << 1 | 1].g);
    return;
}

void update(int rt, int p, int x)
{
    if(t[rt].l == t[rt].r && t[rt].l == p)
    {
        t[rt].g = x;
        return ;
    }
    int mid = t[rt].l + t[rt].r >> 1;
    if(p <= mid) update(rt << 1, p, x);
    else update(rt << 1 | 1, p, x);
    t[rt].g = gcd(t[rt << 1].g, t[rt << 1 | 1].g);
    return;    
}
int summ = 0;
void query(int rt)
{
    if(summ > 1) return ; // ¼ôÖ¦ ºÜÖØÒª 
    if(ll > t[rt].r || rr < t[rt].l) return ;
    int mid = t[rt].r + t[rt].l >> 1;
    
    if(ll <= t[rt].l && rr >= t[rt].r)
    {
        if(t[rt].g % xx == 0) return ;
        else 
        {
            if(t[rt].l == t[rt].r)
            {
                summ++;
                return ;
            }
            query(rt << 1);
            query(rt << 1 | 1);
            return ;
        }
    }
    if(mid >= ll) query(rt << 1);
    if(rr > mid) query(rt << 1 | 1);
    return; 
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    cin>>n;
    for(int i = 1;i <= n;i++) cin>> a[i];
    build(1, 1, n);
    cin>>q;
    for(int i = 1;i <= q;i++)
    {
        cin>>op;
        if(op == 1)
        {
            cin>> ll >> rr >> xx;
            query(1);
            cout<<summ<<" ";
            if(summ <= 1) cout<<"YES\n";
            else cout<<"NO\n";
            summ = 0;
        }
        else 
        {
            cin>> poi >> xx;
            update(1, poi, xx);
        }
    }
    return 0;
}
View Code

 

总结一下写的前三题:

1.小失误太多了,不过这么久没写题了,还上强度,正常,渐渐找回感觉就好

2.鼠标搞我心态,赶紧换个鼠标

3.基础大概都还在,继续学吧

 

D题

维护后缀最大值的哈希(方便比较),如果相同就代表平局,输出 YES

如何维护这样一个后缀最大值见 线段树的用法

目前问题在于如何求哈希,并进行合并。

其实好像也差不多?只要能够维护好这个后缀最大值,想要哈希就很简单了。哈希其实就是一个基于某进制的表示

由于是后缀最大值,那肯定是保留 右区间的后缀最大值,然后找到左区间的后缀最大值(要求均大于等于右边区间的最大值),然后重新计算哈希值就好了

对于下面代码中,cnt用于表示这样一个单调序列的长度,h则表示哈希值。

那么在拼接的时候,就要删去原来的,加上新增的。

对于某一个区间的哈希,右区间的序列在高位,左区间在低位,所以是 mk.h -= `````(mk.cnt - mk.lson.cnt),且左区间的序列中的最小的那一个,一定是大于等于右区间序列中最大的那个的,所以左区间一定是满足的,而右区间需要进一步的划分

然后在merge的时候,左边是低位,右边是高位,所以是 右.h * pow[ 总.cnt - 左.cnt] + 左.h

看上去复杂,实际上很简单,多动脑 !!!

 

 

E题

今天花了一天?总算是看明白这什么意思了,也有可能是因为我今天太摆了(

线段是yyds

建立在一个很重要的结论之上:

要想更新的次数最少,就把负数全放前面,然后正数从小到大排序放后面,假设更新次数为 minn

要想更新次数最多,就从依次把最小的正数放到负数的前面,假设更新次数为 maxn

关键:可以通过操作,得到从minn 到 maxn 的全部序列

如何得到呢。

我们可以考虑先把负数放在一起,n1, n2, n3, ... 

考虑这样一个升序的正数序列,p1, p2, p3, ...

这样组合得到 p1, p2, p3, ...pk, n1, n2, n3 ...,

两个序列的和恰好 小于等于 0,而对于多出的正数,我们将其放在序列的末尾(假设有 L 个),这样得到的可更新次数一定是最多的,也就是 k + L

而如果我们这样排列 n1, n2, n3, ..., p1, p2, p3, ... pk, pk+1,  ...  ,那么这样更新次数一定是最少的,是 L 个

如何转移,只需要依次将 pi ( i = k, k-1, k-2, ... , 1)移到负数序列末尾就可以了。

这样答案就是 k + L - L + 1

问题是要如何求解这个 k,也就是 前 k 小的正数之和 + 负数 的和 <= 0

这样的问题可以转变成求 前 m 大的正数之和 >= 正数与负数之和

那么,是直接使用了权值线段树,虽然不确定会有多少正数确定,但是上限是确定的,最多4e5个,而正数的范围比较大(1e9),所以这里直接用区间来表示这个数是否存在,比如对于区间 [1,1],就说明 1 这个正数存在,而用 num 来表示区间内的数字出现了多少次,sum表示区间中存在的数字之和。

剩下应该没什么需要补充的了。

#include <iostream>
using namespace std;
typedef long long ll;
const int N = 2e5 + 3;
const int INF = 1e9;
namespace SegmentTree
{
    struct node
    {
        int l, r, num;
        ll sum;
    } st[N << 5];
    int tot;
    void update(int &id, int segl, int segr, int pos, int val)
    {
        if (!id)
            id = ++tot; // 离散化?
        st[id].num += val;
        st[id].sum += val * pos;
        if (segl == segr)
            return;
        int mid = (segl + segr) >> 1;
        if (pos <= mid)
            update(st[id].l, segl, mid, pos, val);
        else
            update(st[id].r, mid + 1, segr, pos, val);
    }
    int query(int id, int segl, int segr, ll sum)
    {
        if (segl == segr)
//             return (sum <= 0 ? 0 : (sum + segl - 1) / segl);       
            return (sum <= 0 ? 0 : min(st[id].num, (int)((sum + segl - 1) / segl)));
        int mid = (segl + segr) >> 1;
        // 要前几大的数,所以先和右边区间比较,如果右边不够,加上左边
        if (st[st[id].r].sum >= sum)
            return query(st[id].r, mid + 1, segr, sum);
        return query(st[id].l, segl, mid, sum - st[st[id].r].sum) + st[st[id].r].num;
    }
    void print()
    {
        for(int i = 1;i <= tot;i++)
            cout<<i << " " << st[i].l << " " << st[i].r << " " << st[i].num << " " << st[i].sum << '\n';
        return ;
    }
};

int a[N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    int n, T, rt = 0, cnt = 0;
    ll sum = 0;
    cin >> n >> T;
    for (int i = 1; i <= n; ++i)
    {
        cin >> a[i];
        if (a[i] > 0)
            SegmentTree::update(rt, 1, INF, a[i], 1), ++cnt;
        sum += a[i];
        // 总和 
    }
//    SegmentTree::print();
    for (int kase = 1; kase <= T; ++kase)
    {
        int x, v;
        cin >> x >> v;
        if (a[x] > 0)
            SegmentTree::update(rt, 1, INF, a[x], -1), --cnt; // 删数 
        if (v > 0)
            SegmentTree::update(rt, 1, INF, v, 1), ++cnt; //加数 
        sum = sum - a[x] + v;
//        SegmentTree::print();
        // 更新总和
        // cnt : 正数的个数 
        a[x] = v;
//        cout << sum << '\n';
        cout << cnt - SegmentTree::query(1, 1, INF, sum) +1<< "\n";
    }
    return 0;
}
View Code

 

posted @ 2024-01-20 00:00  是谁不可理喻  阅读(42)  评论(0)    收藏  举报