NOI2022

gtm1514 / 2023-07-14 / 原文

[NOI2022] 众数

首先看到题面第一句话发现这玩意跟摩尔投票一样。然后我们知道摩尔投票有结合律。于是我们如果不考虑前两个操作,那每个序列开个动态开点线段树维护一下摩尔投票,然后合并可以线段树合并,查询直接查每个序列摩尔投票的和,然后再统计一下这个答案在所有序列里的出现次数是否合法即可。

对于前两个操作,可以开个什么东西动态地维护序列,然后合并可以启发式合并。

然后是坑:首先这题赛时爆了一车零,因为 1e6 个 deque 会 MLE。需要 list。

然后是这题赛时一车人挂了 0-30 不等,因为查询的序列可能相同,因此统计次数要开 long long。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#include <list>
using namespace std;
int n,q;
list<int>v[1000010];
struct data{
    int val;
    long long cnt;
    data operator+(data s){
        if(val==s.val)return {val,cnt+s.cnt};
        else{
            if(cnt>s.cnt)return {val,cnt-s.cnt};
            else return {s.val,s.cnt-cnt};
        }
    }
};
struct node{
    #define lson tree[rt].ls
    #define rson tree[rt].rs
    int ls,rs;
    data val;
}tree[1000010<<5];
int t,rt[1000010];
void pushup(int rt){
    tree[rt].val=tree[lson].val+tree[rson].val;
}
void update(int &rt,int L,int R,int pos,int val){
    if(!rt)rt=++t;
    if(L==R){
        tree[rt].val.val=pos;
        tree[rt].val.cnt+=val;
        return;
    }
    int mid=(L+R)>>1;
    if(pos<=mid)update(lson,L,mid,pos,val);
    else update(rson,mid+1,R,pos,val);
    pushup(rt);
}
int query(int rt,int L,int R,int pos){
    if(!rt)return 0;
    if(L==R)return tree[rt].val.cnt;
    int mid=(L+R)>>1;
    if(pos<=mid)return query(lson,L,mid,pos);
    else return query(rson,mid+1,R,pos);
}
int merge(int x,int y,int l,int r){
    if(!x||!y)return x|y;
    if(l==r){
        tree[x].val.cnt+=tree[y].val.cnt;
        return x;
    }
    int mid=(l+r)>>1;
    tree[x].ls=merge(tree[x].ls,tree[y].ls,l,mid);
    tree[x].rs=merge(tree[x].rs,tree[y].rs,mid+1,r);
    pushup(x);
    return x;
}
int main(){
    scanf("%d%d",&n,&q);int lim=n+q;
    for(int i=1;i<=n;i++){
        int l;scanf("%d",&l);
        while(l--){
            int x;scanf("%d",&x);
            v[i].push_back(x);update(rt[i],1,lim,x,1);
        }
    }
    while(q--){
        int od;scanf("%d",&od);
        if(od==1){
            int x,y;scanf("%d%d",&x,&y);
            v[x].push_back(y);
            update(rt[x],1,lim,y,1);
        }
        else if(od==2){
            int x;scanf("%d",&x);
            int y=v[x].back();v[x].pop_back();
            update(rt[x],1,lim,y,-1);
        }
        else if(od==3){
            data val={0,0};
            int m;scanf("%d",&m);
            long long cnt=0,sum=0;
            static int tmp[500010];
            for(int i=1;i<=m;i++){
                scanf("%d",&tmp[i]);
                val=val+tree[rt[tmp[i]]].val;
                sum+=v[tmp[i]].size();
            }
            for(int i=1;i<=m;i++)cnt+=query(rt[tmp[i]],1,lim,val.val);
            if((cnt<<1)<=sum)puts("-1");
            else printf("%d\n",val.val);
        }
        else{
            int x1,x2,x3;scanf("%d%d%d",&x1,&x2,&x3);
            rt[x1]=merge(rt[x1],rt[x2],1,lim);rt[x3]=rt[x1];
            if(v[x1].size()<v[x2].size()){
                while(!v[x1].empty())v[x2].push_front(v[x1].back()),v[x1].pop_back();
                swap(v[x2],v[x3]);
            }
            else{
                while(!v[x2].empty())v[x1].push_back(v[x2].front()),v[x2].pop_front();
                swap(v[x1],v[x3]);
            }
        }
    }
    return 0;
}

[NOI2022] 移除石子

一道玄学的题目。

首先考虑如何判定。先看 \(k=0,l=r\) 的部分,即给定一个序列如何判定是否有解。

观察到操作 \(2\) 只会有 \(3,4,5\) 长度的而且不会重复。

\(dp_{i,j,k}\) 为到 \(i\),钦定有 \(j\)\(i\) 之前的段延长到 \(i\),必须有 \(k\) 个延伸到 \(i+1\) 是否合法。转移考虑初值 \(dp_{1,0,0}=1\),从 \(i\) 转移到 \(i+1\) 时枚举从 \(i\) 开始的操作个数 \(l\),若 \(a_i-j-k-l\) 小于 \(0\) 或等于 \(1\) 则不合法,否则选出 \(p\in[k,k+j]\) 个钦定延伸到 \(i+1\),并使 \(dp_{i+1,p,l}=1\),答案即 \(dp_{n+1,0,0}\)。复杂分析或者打个大表一直拍可以得到 \(j,k\) 只取到 \([0,2]\) 一共九种状态。

考虑 \(k>0\) 的判断。容易发现大部分情况下 \(k\) 合法则 \(>k\) 的数也合法,特例是 \(k=1\) 且全 \(0\)\(k=1,n=3\) 且三个都是 \(1\)。于是设 \(f_{i,j,k}\) 为使 \(dp_{i,j,k}=1\) 至少添加的石子个数,转移同样枚举 \(l\)\(p\),容易得到至少在 \(i\) 出添加多少个石子。检查是否 \(f_{n+1,0,0}\le k\) 即可。

然后是关于计数。先对 \(k=0\) 计数:由于 \(j,k,l\le 2\),因此 \(\ge 8\) 的所有 \(a_i\) 都是等价的。打个大表一直拍可以把这个界减到 \(6\)。于是只要枚举 \([0,6]\)。考虑 dp 套 dp:先暴力搜出 \(a_i\) 为某个数时一种状态会转移到哪种状态。转移枚举状态 \(s\)\(a_i\),对于 \(a_i<6\) 那么只要 \(l_i\le a_i\le r_i\) 就有一倍贡献,\(a_i\ge 6\) 时则每个 \(a_i\) 都有贡献,转移很简单。

把两个东西拼起来,\(k\)\(100\) 所以 \(g\) 的值域是 \([0,101]\),看起来状态数是 \(102^9\)。但是搜一下会发现实际上 \(8000\) 多种,直接跑就行了。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#include <map>
using namespace std;
const int mod=1000000007;
int n,k,num,ans,trans[10010][7],dp[2][10010],l[1010],r[1010];
map<vector<int>,int>mp;
int dfs(vector<int>g){
    if(mp.find(g)!=mp.end())return mp[g];
    mp[g]=num++;int id=mp[g];
    for(int i=0;i<7;i++){
        vector<int>tmp;
        for(int j=0;j<9;j++)tmp.push_back(101);
        for(int j=0;j<3;j++){
            for(int k=0;k<3;k++){
                if(g[3*j+k]==101)continue;
                for(int x=0;x<3;x++){
                    int val=i-j-k-x;
                    if(val<0)val=-val;
                    else val=val==1;
                    for(int l=k;l<=min(2,j+k);l++)tmp[3*l+x]=min(tmp[3*l+x],g[3*j+k]+val);
                }
            }
        }
        trans[id][i]=dfs(tmp);
    }
    return id;
}
int main(){
    int tim;scanf("%d",&tim);
    dfs({0,101,101,101,101,101,101,101,101});
    while(tim--){
        scanf("%d%d",&n,&k);ans=0;
        for(int i=1;i<=n;i++)scanf("%d%d",&l[i],&r[i]);
        memset(dp,0,sizeof(dp));dp[0][0]=1;
        int cur=0;
        for(int i=1;i<=n;i++){
            cur^=1;
            memset(dp[cur],0,sizeof(dp[cur]));
            for(int j=0;j<num;j++){
                if(!dp[cur^1][j])continue;
                for(int k=0;k<7;k++){
                    int ret=0;
                    if(k<6)ret=l[i]<=k&&k<=r[i];
                    else if(r[i]>=6)ret=r[i]-max(l[i],6)+1;
                    dp[cur][trans[j][k]]=(dp[cur][trans[j][k]]+1ll*ret*dp[cur^1][j])%mod;
                }
            }
        }
        for(pair<vector<int>,int>p:mp)if(p.first[0]<=k)ans=(ans+dp[cur][p.second])%mod;
        if(k==1){
            bool jud=true;
            for(int i=1;i<=n;i++)if(l[i]!=0){
                jud=false;break;
            }
            if(jud)ans=(ans-1+mod)%mod;
            if(n==3&&l[1]<=1&&r[1]>=1&&l[2]<=1&&r[2]>=1&&l[3]<=1&&r[3]>=1)ans=(ans-1+mod)%mod;
        }
        printf("%d\n",ans);
    }
    return 0;
}

[NOI2022] 树上邻域数点

不会 Top Tree。

[NOI2022] 挑战 NPC Ⅱ

看到 \(k\) 比较小,于是来个暴力一点的方法。

我们顺序搜两棵树,然后哈希值相同的直接匹配显然是最优的,哈希值不同的可以 \(O(k!)\) 暴力枚举哪对之间匹配然后暴力搜下去。

然后还需要加上一些玄学剪枝:首先如果 \(G\) 的儿子个数 / 子树大小小于 \(H\) 的儿子个数 / 子树大小显然不合法。然后如果子树大小相同直接返回哈希值相同。好像还有别的剪枝,不过不加也没什么关系,反正能过。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
typedef unsigned long long ull;
int n1,n2;
ull nxt(ull x){
	x^=x<<13;x^=x>>7;x^=x<<17;
	return x;
}
ull hs[100010][2];
vector<int>g[100010][2];
bool cmp1(int a,int b){
    return hs[a][0]<hs[b][0];
}
bool cmp2(int a,int b){
    return hs[a][1]<hs[b][1];
}
int size[100010][2];
void dfs1(int x,int id){
    hs[x][id]=size[x][id]=1;
    for(int v:g[x][id]){
        dfs1(v,id);
        hs[x][id]+=nxt(hs[v][id]);
        size[x][id]+=size[v][id];
    }
    if(id==0)sort(g[x][id].begin(),g[x][id].end(),cmp1);
    else sort(g[x][id].begin(),g[x][id].end(),cmp2);
}
bool dfs(int x,int y){
    if(g[x][0].size()<g[y][1].size())return false;
    if(size[x][0]<size[y][1])return false;
    if(size[x][0]==size[y][1])return hs[x][0]==hs[y][1];
    vector<int>A,B;
    int a=0,b=0;
    while(a<g[x][0].size()&&b<g[y][1].size()){
        if(hs[g[x][0][a]][0]==hs[g[y][1][b]][1])a++,b++;
        else if(hs[g[x][0][a]][0]<hs[g[y][1][b]][1])A.push_back(g[x][0][a]),a++;
        else B.push_back(g[y][1][b]),b++;
    }
    while(a<g[x][0].size())A.push_back(g[x][0][a]),a++;
    while(b<g[y][1].size())B.push_back(g[y][1][b]),b++;
    vector<int>p;
    for(int i=0;i<A.size();i++)p.push_back(i);
    do{
        bool jud=true;
        for(int i=0;i<B.size();i++){
            if(size[A[p[i]]][0]<size[B[i]][1]){
                jud=false;break;
            }
        }
        if(!jud)continue;
        for(int i=0;i<B.size();i++){
            if(!dfs(A[p[i]],B[i])){
                jud=false;break;
            }
        }
        if(jud)return true;
    }while(next_permutation(p.begin(),p.end()));
    return false;
}
int main(){
    int tim;scanf("%*d%d%*d",&tim);
    while(tim--){
        scanf("%d",&n1);
        int rt1,rt2;
        for(int i=1;i<=n1;i++){
            int f;scanf("%d",&f);
            if(f==-1)rt1=i;
            else g[f][0].push_back(i);
        }
        scanf("%d",&n2);
        for(int i=1;i<=n2;i++){
            int f;scanf("%d",&f);
            if(f==-1)rt2=i;
            else g[f][1].push_back(i);
        }
        dfs1(rt1,0);dfs1(rt2,1);
        if(dfs(rt1,rt2))puts("Yes");
        else puts("No");
        for(int i=1;i<=n1;i++)g[i][0].clear();
        for(int i=1;i<=n2;i++)g[i][1].clear();
    }
}

[NOI2022] 冒泡排序

首先显然可以让所有数只取到给定的 \(V\) 中的数。于是先离散化一下。然后最小化逆序对个数。

先看 B 部分分。确定了一堆单点,那么开个区间加查最小值的线段树随便扫一扫就行了。

然后是 C。容易发现把区间的最小值放在开头一定最优,于是变成了确定一堆单点,每个位置 \(\ge\) 某个数。也是线段树扫一扫就行了。

最后是比较困难的 A。根据上边的性质,我们必须把 \(1\) 铺满。然后看剩下的 \(0\) 的区间,它就变成了 C 部分。在开头安上然后线段树扫一扫就行了。

于是正解就十分显然了:我们把 \(V\) 从大到小排序,每次扫所有 \(\ge V\) 且没扫过的位置(可以用并查集记录扫没扫),在开头确定为最小值,然后给剩下的位置打个 \(\ge\) 这个最小值的标记。确定的部分可以树状数组扫一遍统计,不确定的线段树扫一扫。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
int n,m,lsh[1000010];
long long ans;
struct ques{
    int l,r,val;
}q[1000010];
vector<pair<int,int> >v[1000010];
struct BIT{
    int c[1000010];
    #define lowbit(x) (x&-x)
    void update(int x){
        while(x)c[x]++,x-=lowbit(x);
    }
    int query(int x){
        int sum=0;
        while(x<=m)sum+=c[x],x+=lowbit(x);
        return sum;
    }
}c;
int fa[1000010];
int find(int x){
    return x==fa[x]?fa[x]:fa[x]=find(fa[x]);
}
struct node{
    #define lson rt<<1
    #define rson rt<<1|1
    int mn,pos,lz;
}tree[4000010];
void pushup(int rt){
    tree[rt].mn=min(tree[lson].mn,tree[rson].mn);
    if(tree[rt].mn==tree[lson].mn)tree[rt].pos=tree[lson].pos;
    else tree[rt].pos=tree[rson].pos;
}
void pushtag(int rt,int val){
    tree[rt].mn+=val;tree[rt].lz+=val;
}
void pushdown(int rt){
    if(tree[rt].lz){
        pushtag(lson,tree[rt].lz);
        pushtag(rson,tree[rt].lz);
        tree[rt].lz=0;
    }
}
void build(int rt,int l,int r){
    tree[rt].mn=tree[rt].lz=0;tree[rt].pos=l;
    if(l==r)return;
    int mid=(l+r)>>1;
    build(lson,l,mid);build(rson,mid+1,r);
}
void update(int rt,int L,int R,int l,int r,int val){
    if(l<=L&&R<=r){
        pushtag(rt,val);return;
    }
    pushdown(rt);
    int mid=(L+R)>>1;
    if(l<=mid)update(lson,L,mid,l,r,val);
    if(mid<r)update(rson,mid+1,R,l,r,val);
    pushup(rt);
}
pair<int,int> query(int rt,int L,int R,int l,int r){
    if(l<=L&&R<=r)return make_pair(tree[rt].mn,tree[rt].pos);
    pushdown(rt);
    int mid=(L+R)>>1;
    pair<int,int>val=make_pair(0x3f3f3f3f,0x3f3f3f3f);
    if(l<=mid)val=min(val,query(lson,L,mid,l,r));
    if(mid<r)val=min(val,query(rson,mid+1,R,l,r));
    return val;
}
int val[1000010],mn[1000010];
void solve(){
    scanf("%d%d",&n,&m);ans=0;
    for(int i=1;i<=n+1;i++)fa[i]=i,val[i]=mn[i]=0;
    for(int i=1;i<=m;i++)c.c[i]=0,v[i].clear();
    for(int i=1;i<=m;i++)scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].val),lsh[i]=q[i].val;
    sort(lsh+1,lsh+m+1);
    for(int i=1;i<=m;i++){
        q[i].val=lower_bound(lsh+1,lsh+m+1,q[i].val)-lsh;
        v[q[i].val].push_back(make_pair(q[i].l,q[i].r));
    }
    for(int i=m;i>=0;i--){
        if(v[i].empty())continue;
        sort(v[i].begin(),v[i].end());
        int pre=n+1;
        for(int j=v[i].size()-1;j>=0;j--){
            if(v[i][j].second<pre){
                int pos=find(v[i][j].first);
                if(pos>v[i][j].second){
                    puts("-1");return;
                }
                val[pos]=i;pre=pos;
            }
        }
        for(pair<int,int>p:v[i]){
            for(int j=find(p.first);j<=p.second;j=find(j))mn[j]=i,fa[j]=j+1;
        }
    }
    build(1,0,m+1);
    for(int i=1;i<=n;i++){
        if(val[i]){
            ans+=c.query(val[i]+1);
            c.update(val[i]);
            update(1,0,m+1,val[i]+1,m+1,1);
        }
    }
    for(int i=1;i<=n;i++){
        if(val[i]){
            update(1,0,m+1,val[i]+1,m+1,-1);
            update(1,0,m+1,0,val[i]-1,1);
        }
        else{
            pair<int,int>p=query(1,0,m+1,mn[i],m+1);
            ans+=p.first;
            if(p.second)update(1,0,m+1,0,p.second-1,1);
        }
    }
    printf("%lld\n",ans);
}
int main(){
    int tim;scanf("%d",&tim);
    while(tim--)solve();
    return 0;
}

[NOI2022] 二次整数规划问题

(原先是 10 个,由于要放假了于是加 10 个)