HNUCM2020年春季ACM集训队热身赛-第5场题解

问题 A: 质数因子

题目描述

功能:输入一个正整数,按照从小到大的顺序输出它的所有质因子(如180的质因子为2 2 3 3 5 ),
最后一个数后面也要有空格。

输入

多组输入,每组输入一个整数n(1

输出

每组数据输出一行。
每行按照从小到大的顺序输出它的所有质数的因子,以空格隔开。最后一个数后面也要有空格。

样例输入

180

样例输出

2 2 3 3 5

思路

2到sqrt(n)循环,判断i是否能够整除n,如果能循环除i
//如果能整除,这个i肯定是质数。
//反证法:如果不是,那在i前,n会被i的因子除,i也就不能整除n了

i=2;i * i<=n;++i

//每次被除,n都会改变,但如果i*i大于n,也就是i大于sqrt(n)说明n已经是质数,直接输出就好
//反证法:如果i整除n而且i>sqrt(n)也就是 i * j=n,j也就小于sqrt(n)在i之前就会遍历到j,所以不成立

#include 
#define ll long long
using namespace std;
const int maxn=2e5+5;
int main(){
    ll n;
    while(~scanf("%lld",&n)){
        for(ll i=2;i*i<=n;++i){
            while(n%i==0){
                n/=i;
                printf("%lld ",i);
            }
        }
        if(n>1){
            printf("%lld ",n);
        }
        putchar('\n');
    }
    return 0;
}

问题 B: 取近似值

题目描述

写出一个程序,接受一个正浮点数值,输出该数值的近似整数值。如果小数点后数值大于等于5,向上取整;小于5,则向下取整。

输入

多组输入,每组输入一个正浮点数值。

输出

每组输出该数值的近似整数值。

样例输入

5.5

样例输出

6

代码1

将n加0.5再强转舍去小数

#include 
using namespace std;
int main()
{
    double n;
    while(~scanf("%lf",&n)){
        printf("%d\n",(int)(n+0.5));
    }
    return 0;
}
代码2

n*10/10得到小数点后第一个数,判断一下是否大于等于5

#include 
using namespace std;
int main()
{
    double n;
    while(~scanf("%lf",&n)){
        if(((int)(n*10)%10)>=5)
            printf("%d\n",(int)n+1);
        else
            printf("%d\n",(int)n);
    }
    return 0;
}

问题 C: 合并表记录

题目描述

数据表记录包含表索引和数值(int范围的整数),请对表索引相同的记录进行合并,即将相同索引的数值进行求和运算,输出按照key值升序进行输出。

输入

先输入键值对的个数n;
然后输入成对的index和value值,以空格隔开。
题目保证n<=1e5,且index,value均在int范围

输出

输出合并后的键值对(多行)。

样例输入

4
0 1
0 2
1 2
3 4

样例输出

0 3
1 2
3 4

思路

用map储存每一种index的value
数组的话比较麻烦,可能有负的index,而且如果数据很极限有-100000000 和100000000那会处理不了

#include 
using namespace std;
int main()
{
    int n,a,b;
    while(~scanf("%d",&n)){
        map<int,ll> mp;
        map<int,ll>::iterator it;
        while(n--){
            scanf("%d %d",&a,&b);
            mp[a]+=b;
        }
        for(it=mp.begin();it!=mp.end();++it){
            printf("%d %lld\n",it->first,it->second);
        }
    }
    return 0;
}

问题 D: 提取不重复的整数

题目描述

输入一个int型整数,按照从右向左的阅读顺序,返回一个不含重复数字的新的整数。

输入

多组输入,每行输入一个int型整数。

输出

按照从右向左的阅读顺序,每组数据返回一个不含重复数字的新的整数,注意去除前导0。

样例输入

9876673

样例输出

37689

思路

按题目要求从右至左遍历n,用一个变量标记是否输出过,输出过才能输出0(去除前导0)
ps:1010应该输出1

#include 
using namespace std;
int main()
{
    int n,x;
    while(~scanf("%d",&n)){
        int ju=0,mp[15]={0};
        while(n){
            x=n%10;n/=10;
            if(mp[x]==0){
                mp[x]=1;
                if(x==0&&ju==0)
                    continue;
                ju=1;
                printf("%d",x);
            }
        }
        if(ju==0)printf("0");//如果没有输出过,也就是都是0
        putchar('\n');
    }
    return 0;
}

问题 E: 字符个数统计

题目描述

编写一个函数,计算字符串中含有的不同字符的个数。字符在ACSII码范围内(0~127),换行表示结束符,不算在字符里。不在范围内的不作统计。

输入

输入N个字符,字符在ACSII码范围内。(1<=N<=1e5)

输出

输出范围在(0~127)字符的个数。

样例输入

abc

样例输出

3

思路

判断字符是否属于区间(0,127)
再用数组标记字符是否累加过,没有则ans++,再标记

#include 
using namespace std;
const int maxn=1e5+5;
char s[maxn];
int main()
{
    while(gets(s+1)){
        int ls=strlen(s+1),ans=0,mp[130]={0};
        for(int i=1;i<=ls;++i){
            if(s[i]>0&&s[i]<127&&mp[s[i]]==0)++ans,mp[s[i]]=1;
        }
        printf("%d\n",ans);
    }
    return 0;
}

问题 F: Graph

题目描述

给出 N 个点,M 条边的有向图,对于每个点 v,求 A(v) 表示从点 v 出发,能到达的编号最大的点。

输入

第 1 行,2 个整数 N,M。 接下来 M 行,每行 2 个整数,Ui,Vi表示边(Ui,Vi)。点用 1,2,…,N 编号。(1≤N,M≤105)

输出

N 个整数 A(1),A(2),⋯,A(N)。

样例输入

4 3
1 2
2 4
4 3

样例输出

4 4 3 4

思路

可能有环,正向建边遍历很麻烦。
考虑反向建边,编号从大到小遍历进行dfs,将当前编号能到达且未被遍历到的点标记为当前编号
如果dfs的时候,一个点已经被标记过了说明他能到比当前dfs的编号更大或者相等的点,直接return

#include 
#define ll long long
using namespace std;
const int maxn=1e5+5;
vector<int>w[maxn];
int ans[maxn];
void dfs(int now,int v){
    if(ans[now])return;
    ans[now]=v;
    for(int i=0;i<w[now].size();++i){
        int to=w[now][i];
        dfs(to,v);
    }
}
int main()
{
    int n,m,a,b;
    while(~scanf("%d %d",&n,&m)){
        memset(ans,0,sizeof(ans));
        for(int i=1;i<=n;++i)w[i].clear();
        for(int i=1;i<=m;++i){
            scanf("%d %d",&a,&b);
            w[b].push_back(a);
        }
        for(int i=n;i>=1;--i){
            if(ans[i]==0)dfs(i,i);
        }
        for(int i=1;i<=n;++i){
            printf("%d ",ans[i]);
        }
    }
    return 0;
}

问题 G: Sequence

题目描述

给出序列A1,A2,AN求1≤i≤j≤N∑(Ai⊕Ai+1⊕⋯⊕Aj)的值。其中,⊕ 表示按位异或。

输入

第 1 行,1 个整数 N。 第 2 行,N 个整数A1,A2,AN 。(1≤N≤105,0≤Ai≤109)

输出

1 个整数。

样例输入

2
1 2

样例输出

6

思路

二进制和前缀思想
将数字转化为二进制,对每一位进行考虑。
假如n=7
这个7个数的某一位上的情况为
1 0 0 1 0 1 0
计算前缀pre[i](当前点前的1的个数)
0 1 1 1 2 2 3 3(第一个0是什么都没有的情况)
奇偶性
0 1 1 1 0 0 1 1
对于Ai⊕Ai+1⊕⋯⊕Aj,如果区间i到j的1为奇则对答案有贡献
也就是说j的前缀pre[j]减去i-1的前缀pre[i-1]为奇数,也就是pre[j]减pre[i-1]的奇偶性为奇
即pre[j]和pre[i-1]的奇偶性不同,这一位为1的情况也就是((这n个数前缀为奇的数量)乘(这n个数前缀为偶的数量加1)) (加1是空的前缀,也为偶)

#include 
#define ll long long
using namespace std;
const int maxn=1e5+5;
int a[maxn];
ll num[35];
int main()
{
    int n,now=0;
    scanf("%d",&n);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    for(int i=1;i<=n;++i){
        now^=a[i];
        for(int j=0;j<=30;++j){
            num[j]+=(now>>j)&1;
        }
    }
    ll ans=0;
    for(int i=0;i<=30;++i){
        ans+=(num[i]*(n+1-num[i]))<<i;
    }
    printf("%lld\n",ans);
    return 0;
}

问题 H: Inversion

题目描述

对于序列 A,它的逆序对数定义为满足iAj的数对 (i,j ) 的个数。
现给你 1 到 n 的一个排列,并按照某种顺序依次删除 m 个元素。现请你求出在每次删除一个元素之前整个序列的逆序对总个数。

输入

第一行包含两个整数 n 和 m
第二行包含 n 个数,代表初始序列
接下来的 m 行,每行一个整数表示第 i 次删除的数(保证在此之前未曾删除过)
1≤n≤1051≤m≤5×104

输出

共 m 行,表示第 i 次删数前整个序列的逆序对总个数。

样例输入

5 4
1 5 3 4 2
5
1
4
2

样例输出

5
2
2
1

思路

解法较多,树套树什么的
这里介绍一下CDQ分治

二维偏序问题:
对于每个元素(a,b),有多少个有序对(a0,b0)满足a0 < a且b0 < b
求逆序对其实就是一个二维偏序问题
二维偏序问题几何表现为在二维坐标上求一个矩形内的点数
HNUCM2020年春季ACM集训队热身赛-第5场题解_第1张图片
每一个位置由两个元素表示,x表示坐标,y表示大小,求逆序对就是求有多少个a,b满足a.xb.y

但这里需要删除怎么办呢
我们可以再加一维,时间维,变成三维偏序问题(给定N个有序三元组(a,b,c),求对于每个三元组(a,b,c),有多少个三元组(a0,b0,c0)满足a0 < a且b0 < b且c0 < c)。
二维偏序问题几何表现为在三维坐标上求一个长方体内的点数
HNUCM2020年春季ACM集训队热身赛-第5场题解_第2张图片
每一个位置由三个元素表示,x表示坐标,y表示大小,t表示元素出现的时间
先删除的时间维度大,后删除的时间维度小
问题就转变成求有多少个a,b满足a.xb.y&&a.t>b.t
具体的还是看代码实现吧…

第一次看到实现的代码确实很令人头疼~我也是看了很久才弄懂

#include 
#define ll long long
using namespace std;
const int maxn=1e5+5;
struct node{
	int x,y,t;
}a[maxn],b[maxn];
ll ans[maxn];
int c[maxn],le[maxn],ri[maxn],pos[maxn],n;
//树状数组添加
void add(int i,int v){
	while(i<=n){
		c[i]+=v;
		i+=i&(-i);
	}
}
//树状数组查询
int sum(int i){
	int num=0;
	while(i>0){
		num+=c[i];
		i-=i&(-i);
	}
	return num;
}
//cdq分治
void cdq(int l,int r){
	if(l>=r)return;
	int mid=(l+r)>>1,l1=l,l2=mid+1;
	//将区间l和r按时间戳分成两部分
	//可以发现前一半的时间戳比后一半任意一个时间戳都要小,前一部分和后一部分的x都分别是有序的
	for(int i=l;i<=r;++i){
		if(a[i].t<=mid)b[l1++]=a[i];
		else b[l2++]=a[i];
	}
	for(int i=l;i<=r;++i)a[i]=b[i];
	//i遍历后一半,寻找前一半里有多少个坐标小于第i个,值大于第i个的数量
	l1=l;
	for(int i=mid+1;i<=r;++i){
		while(l1<=mid&&b[l1].x<b[i].x){
			add(b[l1++].y,1);
		}
		//前一半的总数量减去值小于第i个的数量=前一半里值大于第i个的数量
		le[b[i].t]+=l1-l-sum(b[i].y);
	}
	//复原树状数组
	for(int i=l;i<=l1-1;++i)add(b[i].y,-1);
	//i逆序遍历后一半,寻找前一半里有多少个坐标大于第i个,值小于第i个的数量
	l1=mid;
	for(int i=r;i>=mid+1;--i){
		while(l1>=l&&b[l1].x>b[i].x){
			add(b[l1--].y,1);
		}
		ri[b[i].t]+=sum(b[i].y-1);
	}
	//复原树状数组
	for(int i=l1+1;i<=mid;++i)add(b[i].y,-1);
	cdq(l,mid),cdq(mid+1,r);//对左右部分求逆序对
}
int main()
{
	int m,x;
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i].y),a[i].x=i,pos[a[i].y]=i;//pos[i]表示值为i的元素的坐标
	}
	int tim=n;
	for(int i=1;i<=m;++i){//将需要删除的点加上时间戳
		scanf("%d",&x);
		a[pos[x]].t=tim--;
	}
	for(int i=1;i<=n;++i){//不需删除的点加上时间戳
		if(a[i].t==0)
		a[i].t=tim--;
	}
	cdq(1,n);
	//ri[i]表示时间戳为i的点右边比该点大的数量,le[i]表示时间戳为i的点左边比该点小的数量
	for(int i=1;i<=n;++i) ans[i]=ans[i-1]+ri[i]+le[i];
	for(int i=n;i>=n-m+1;--i)//输出最后m个时间戳的逆序对
		printf("%lld\n",ans[i]);
    return 0;
}

你可能感兴趣的:(HNUCM2020年春季ACM集训队热身赛-第5场题解)