2021南京(重温经典)

本文详细介绍了2021年ICPC亚洲南京区域赛的三道算法题目,包括思维、树状数组和记忆化搜索等知识点的应用。题目C要求通过一次操作找到序列中众数的最大个数;题目D涉及单增序列的处理和交换次数计算;题目J探讨了如何通过最少操作使两数之一变为1。文章提供了每题的思路、代码实现和解题技巧,并附有相关参考文献。
摘要由CSDN通过智能技术生成

2021南京(重温经典)

导语

打的不好,本来以为训练了这么长时间会有不少效果,但是被打的稀里哗啦,思维没有打开

涉及的知识点

思维,树状数组,记忆化搜索

链接:The 2021 ICPC Asia Nanjing Regional Contest (XXII Open Cup, Grand Prix of Nanjing)

题目

C

题目大意:给出一个序列 a 1 , a 2 , … a n a_1,a_2,\dots a_n a1,a2,an和一个整数 k k k,现在可以对序列的任意长度的区间都加上这个 k k k,但最多只能执行一次(也可以不进行这个操作),求出操作之后得到的众数的最大个数

思路:对于每一个 x x x x − k x-k xk考虑构造,因为彼此间会产生影响,设 s u m [ i ] [ 0 ] sum[i][0] sum[i][0]为前i中x的个数, s u m [ i ] [ 1 ] sum[i][1] sum[i][1]为x-k个数,那么对于一个区间 [ l , r ] [l,r] [l,r],那么从x-k变到x得到的贡献为 s u m [ r ] [ 1 ] − s u m [ l − 1 ] [ 1 ] sum[r][1]-sum[l-1][1] sum[r][1]sum[l1][1],这中间减少的x的贡献为 s u m [ r ] [ 0 ] − s u m [ l − 1 ] [ 0 ] sum[r][0]-sum[l-1][0] sum[r][0]sum[l1][0],那么可以得到挑选当前区间得到的总贡献:
s u m [ l e n ] [ 0 ] + s u m [ r ] [ 1 ] − s u m [ l − 1 ] [ 1 ] − s u m [ r ] [ 0 ] + s u m [ l − 1 ] [ 0 ] sum[len][0]+sum[r][1]-sum[l-1][1]-sum[r][0]+sum[l-1][0] sum[len][0]+sum[r][1]sum[l1][1]sum[r][0]+sum[l1][0]
移项可得
s u m [ l e n ] [ 0 ] + s u m [ r ] [ 1 ] − s u m [ r ] [ 0 ] + s u m [ l − 1 ] [ 0 ] − s u m [ l − 1 ] [ 1 ] sum[len][0]+sum[r][1]-sum[r][0]+sum[l-1][0]-sum[l-1][1] sum[len][0]+sum[r][1]sum[r][0]+sum[l1][0]sum[l1][1]
这样在处理r的时候也就顺带获得了l-1的数值,详见代码
代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=4e6+5;
const int M=1e6+9;
const int offset=2e6;
int n,k,ans,cnt,a[M],mx,sum[M][2];
vector<int>v[N];
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>n>>k;
    for(int i=1; i<=n; i++) {
        cin >>a[i];
        a[i]+=offset;
        v[a[i]].push_back(a[i]),v[a[i]+k].push_back(a[i]);
        //a[i]在这个位置有一个值,a[i]+k可以在这个位置有一个值
        mx=max(mx,max((int)v[a[i]].size(),(int)v[a[i]+k].size()));//记录初始的最大值
    }
    if(!k) {
        cout <<mx/2<<endl;//会重复计算,需要去重
        return 0;
    }
    for(int i=0; i<=4e6; i++) {
        int len=v[i].size();//获得数量
        if(len==0)continue;
        for(int j=0; j<len; j++) {//遍历其每一个位置
            sum[j+1][0]=sum[j][0]+(v[i][j]==i);//记录前j个位置有多少个i
            sum[j+1][1]=sum[j][1]+(v[i][j]!=i);//记录前j个位置有多少个i-k
        }
        int tmp=0;
        ans=max(ans,sum[len][0]);//获得最大值
        for(int j=1; j<=len; j++) {
            tmp=max(tmp,sum[j-1][0]-sum[j-1][1]);//获得公式后缀最大值
            ans=max(ans,sum[len][0]+sum[j][1]-sum[j][0]+tmp);//获得公式最值
        }
    }
    cout <<ans<<endl;
    return 0;
}

D

题目大意:略

思路:对于每个长度的第一轮循环之后,结果是将其严格单增序列相对后移(这里的相对后移指的是单增序列的相对位置),之后第2,3…轮排序的交换次数就是前i个比a[i]大的个数(去重)
因此为了获得最后的解,在扫描的时候可以直接在线处理,保证每次只获得当前长度下第一次循环的结果,当遇到a[i]>a[1]时,就交换a[i]和a[1]的位置,这就是一次必要的交换,计数器增加,然后对于每个在线处理录入的a[i],统计先前比它大的个数,直接加上即可
但是,当录入的a[i]与a[1](当前最大值)相等(假设值为x)并且后面存在比a[1]更大的数(假设值为y),那么在后面的长度当中,第一轮排序会使得当前的a[i]的值不变,而a[1]对应的值被移到了更大的数位置上,就是这样的情况:x,x,y->y,x,x,那么在插入y之后,由于y前移了,y对x也有一个需要交换的贡献,举例如下,假设当前长度构造的序列为
x , a , a , a , x , x − 1 , x − 2 , x − 1 x,a,a,a,x,x-1,x-2,x-1 x,a,a,a,x,x1,x2,x1
插入y之后,由于y大于x
y , a , a , a , x , x − 1 , x − 2 , x − 1 , x y,a,a,a,x,x-1,x-2,x-1,x y,a,a,a,x,x1,x2,x1,x
那么对于 x , x − 1 , x − 2 , x − 1 x,x-1,x-2,x-1 x,x1,x2,x1这几个数,都需要加上 y y y的这个贡献,所有需要增加一个计数器,记录第二个x到y间有几个数,插入y的时候结果加上计数器值即可

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n,a[maxn],t,tree[maxn];
bool vis[maxn];
int lowbit(int x) {
    return x&(-x);
}
void Add(int x) {
    for(int i=x; i<=n; i+=lowbit(i))
        tree[i]++;
}
int Sum(int x) {
    int ans=0;
    for(int i=x; i; i-=lowbit(i))
        ans+=tree[i];
    return ans;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>t;
    while(t--) {
        cin >>n;
        int res=0,cnt=0;
        bool flag=0;
        memset(tree,0,sizeof(tree));
        memset(vis,0,sizeof(vis));//初始化
        for(int i=1; i<=n; i++)//录入数据
            cin >>a[i];
        cout <<0;//第一个结果
        vis[a[1]]=1;//代表已经访问过
        Add(a[1]);//加进来
        for(int i=2; i<=n; i++) {
            if(!vis[a[i]])vis[a[i]]=1,Add(a[i]);//未访问插入
            if(a[i]==a[1])flag=1;//标记存在多个最值
            cnt+=flag-(flag?a[i]>a[1]:0);//未统计的贡献
            if(a[i]>a[1])res+=1+cnt,swap(a[1],a[i]),cnt=flag=0;
            //a[1]永远只存最大的数字
            res+=Sum(a[1])-Sum(a[i]);//在i之前出现的比a[i]大的数字个数
            cout <<" "<<res;
        }
        cout <<endl;
    }
    return 0;
}

J

题目大意:给出两个正整数a,b,每次可以进行三种操作:都+1,都-1,都除以一个它们共同的质数因子,判断最少多少次能使得其中一个数为1

思路:如果不进行除法操作,那么ab差值是固定的,如果存在一个数能够整除a和b,那么一定也能整除它们的差值,那么就可以将其差值进行质因子分解,将ab中的较小数加/减到距离质因子最小的数,对a,b及差值同时除质因子,以此反复即可,当无质因子时只能一直减了

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1e5+5;
int n,a,b,t;
vector<int>v;
unordered_map<int,int>level;
int gethash(int b,int c) {//哈希
    return b*1e9+c;
}
int DFS(int b,int c) {
    if(b==1)return 0;
    if(c==1)return b-1;//差值为1,只能一直减
    if(level[gethash(b,c)])return level[gethash(b,c)];//记忆化搜索
    int mn=b-1;//至少需要的次数
    for(auto i:v)//遍历所有的质因子
        if(c%i==0) {//如果还没有除过
            int ans=b%i;//获得差距
            mn=min({mn,ans+1+DFS(b/i,c/i),i-ans+1+DFS(b/i+1,c/i)});
            //分别是b大于i的情况和b小于i的情况,+1是因为进行了一次除
        }
    return level[gethash(b,c)]=mn;
}
signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin >>t;
    while(t--) {
        cin >>a>>b;
        if(a<b)swap(a,b);//强制b为小值
        int c=a-b;
        v.clear();
        for(int i=2; i<=c/i; i++)
            if(c%i==0) {//获得质因子
                v.push_back(i);
                while(c%i==0)c/=i;
            }
        if(c>1)v.push_back(c);//最后除剩的也是质因子
        cout <<DFS(b,a-b)<<endl;//记忆化搜索
    }
    return 0;
}

参考文献

  1. 2021 46届icpc 南京
  2. 【题目记录】——ICPC南京2021
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值