2020年第一场NOI Online提高组

2020年第一场NOI Online提高组比赛,涉及P1427序列、P1428冒泡排序和P1429最小环问题。对于序列,可通过找规律获得分数;冒泡排序通过树状数组优化解决,利用逆序对数量变化进行模拟;最小环问题采用数学和两重贪心策略,寻找最佳配对以最大化乘积。
摘要由CSDN通过智能技术生成

2020年第一场NOI Online提高组

P1427. 序列

题目:序列
思路:

可以直接通过简单的找规律拿到65分

70分代码:
#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N = 1e5 + 10;

int a[N], b[N];
int t[N], u[N], v[N];
int n, m;
int p[N]; //并查集 
int w[N];
bool flag1, flag2;

int find(int x)
{
	if (p[x] != x) p[x] = find(p[x]);
	return p[x];
}

void merge(int x, int y)
{
	int fx = find(x);
	int fy = find(y);
	w[fy] += w[fx];
	w[fx] = 0;
	p[fx] = fy;
}

void init()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]);
	for (int i = 1; i <= m; i ++ )
	{
		scanf("%d%d%d", &t[i], &u[i], &v[i]);
		if (t[i] == 1) flag1 = true;
		if (t[i] == 2) flag2 = true;
	}
}

void solve1() //找规律1 
{
	if (flag1)
	{
	    puts("YES");
	    return;
	}
	if(a[1] == b[1])
	{
	    puts("YES");
	    return;
	}
	puts("NO");
}

void solve2() //找规律2 
{
	if(n <= 2)
	{
		if (flag1)
		{
			if ((a[1] - b[1]) == (a[2] - b[2]))
			{
			    puts("YES");
                return;
			}
		}
		
		if (flag2)
		{
			if ((a[1] + a[2]) == (b[1] + b[2]))
			{
			    puts("YES");
                return;
			}
	
		}
		
		if(flag1 && flag2)
		{
			if((a[1] - b[1] + a[2] - b[2]) % 2 == 0)
			{
			    puts("YES");
                return;
			}
		}
		puts("NO");
	}	
}

void solve3() //并查集
{
	for (int i = 1; i <= n; i ++ )
	{
		w[i] = a[i];
		w[i + n] = b[i];
	}
	
	for (int i = 1; i <= 2 * n; i ++ ) p[i] = i; //初始化并查集
	
	for (int i = 1; i <= m; i ++ )
	{
		int x = u[i], y = v[i];
		if (find(x) == find(y)) continue; //x和y相等,相当于没操作 
		merge(x, y); //合并a 
		merge(x + n, y + n); //合并b 
	}

	for (int i = 1; i <= n; i ++ )
	{
		if (find(i) == i) //只用看每个点的父节点就行了 
		{
			if (w[i] != w[i + n])
			{
			    puts("NO");
			    return;
			}
		}
	}
	puts("YES");
}

int main(){
	int T;
	scanf("%d", &T);
	while (T -- )
	{
	    flag1 = flag2 = false;
		init();		
		
		if (n == 1)
		{
		    solve1();
		    continue;
		}
		
		if (n == 2)
		{
			solve2();
			continue;
		}
		
		if (!flag1 && flag2)
		{
			solve3();
			continue;
		}
	}

	return 0;
} 

P1428. 冒泡排序

题目:冒泡排序
思路:

找规律 + 树状数组优化

before[i]表示所有满足 j<i 且 p[j]>p[i] 的 j 的数量,可以看出所有before[i]相加,就是一开始总的逆序对数量了,每次冒泡之后,before数组中所有大于等于1的元素减一,并全部向左移一位,即每一遍冒泡排序都会使得所有before[i] = max(before[i] - 1, 0),这里不必考虑左移,因为无论是否左移总数是不变的

先模拟一下,举个例子:
p [ 10 ] = { 7 , 4 , 8 , 3 , 1 , 6 , 2 , 5 , 10 , 9 } p[10] = \left\{7,4,8,3,1,6,2,5,10,9 \right\} p[10]={7,4,8,3,1,6,2,5,10,9}
b e f o r e [ 10 ] = { 0 , 1 , 0 , 3 , 4 , 2 , 5 , 3 , 0 , 1 } before[10]=\left\{0,1,0,3,4,2,5,3,0,1\right\} before[10]={0,1,0,3,4,2,5,3,0,1}

第一次冒泡:
p [ 10 ] = { 4 , 7 , 3 , 1 , 6 , 2 , 5 , 8 , 9 , 10 } p[10] = \left\{4,7,3,1,6,2,5,8,9,10 \right\} p[10]={4,7,3,1,6,2,5,8,9,10}
b e f o r e [ 10 ] = { 0 , 0 , 2 , 3 , 1 , 4 , 2 , 0 , 0 , 0 } before[10]= \left\{0,0,2,3,1,4,2,0,0,0 \right\} before[10]={0,0,2,3,1,4,2,0,0,0}

第二次冒泡:
p [ 10 ] = { 4 , 3 , 1 , 6 , 2 , 5 , 7 , 8 , 9 , 10 } p[10]= \left\{4,3,1,6,2,5,7,8,9,10 \right\} p[10]={4,3,1,6,2,5,7,8,9,10}
b e f o r e [ 10 ] = { 0 , 1 , 2 , 0 , 3 , 1 , 0 , 0 , 0 , 0 } before[10]= \left\{0,1,2,0,3,1,0,0,0,0 \right\} before[10]={0,1,2,0,3,1,0,0,0,0}

第三次冒泡:
p [ 10 ] = { 3 , 1 , 4 , 2 , 5 , 6 , 7 , 8 , 9 , 10 } p[10]= \left\{3,1,4,2,5,6,7,8,9,10 \right\} p[10]={3,1,4,2,5,6,7,8,9,10}
b e f o r e [ 10 ] = { 0 , 1 , 0 , 2 , 0 , 0 , 0 , 0 , 0 , 0 } before[10]= \left\{0,1,0,2,0,0,0,0,0,0 \right\} before[10]={0,1,0,2,0,0,0,0,0,0}

第四次冒泡:
p [ 10 ] = { 1 , 3 , 2 , 4 , 5 , 6 , 7 , 8 , 9 , 10 } p[10]= \left\{1,3,2,4,5,6,7,8,9,10 \right\} p[10]={1,3,2,4,5,6,7,8,9,10}
b e f o r e [ 10 ] = { 0 , 0 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } before[10]= \left\{0,0,1,0,0,0,0,0,0,0 \right\} before[10]={0,0,1,0,0,0,0,0,0,0}

第五次冒泡:
p [ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 } p[10]= \left\{1,2,3,4,5,6,7,8,9,10 \right\} p[10]={1,2,3,4,5,6,7,8,9,10}
b e f o r e [ 10 ] = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 } before[10]= \left\{0,0,0,0,0,0,0,0,0,0 \right\} before[10]={0,0,0,0,0,0,0,0,0,0}
结束

如果还没懂的话,再举个例子:
p [ 5 ] = { 5 , 4 , 3 , 2 , 1 } p[5]= \left\{5,4,3,2,1 \right\} p[5]={5,4,3,2,1}
b e f o r e [ 5 ] = { 0 , 1 , 2 , 3 , 4 } before[5]= \left\{0,1,2,3,4 \right\} before[5]={0,1,2,3,4}

第一次冒泡:
p [ 5 ] = { 4 , 3 , 2 , 1 , 5 } p[5]= \left\{4,3,2,1,5 \right\} p[5]={4,3,2,1,5}
b e f o r e [ 5 ] = { 0 , 1 , 2 , 3 , 0 } before[5]= \left\{0,1,2,3,0 \right\} before[5]={0,1,2,3,0}

第二次冒泡:
p [ 5 ] = { 3 , 2 , 1 , 4 , 5 } p[5]= \left\{3,2,1,4,5 \right\} p[5]={3,2,1,4,5}
b e f o r e [ 5 ] = { 0 , 1 , 2 , 0 , 0 } before[5]= \left\{0,1,2,0,0 \right\} before[5]={0,1,2,0,0}

第三次冒泡:
p [ 5 ] = { 2 , 1 , 3 , 4 , 5 } p[5]= \left\{2,1,3,4,5 \right\} p[5]={2,1,3,4,5}
b e f o r e [ 5 ] = { 0 , 1 , 0 , 0 , 0 } before[5]= \left\{0,1,0,0,0 \right\} before[5]={0,1,0,0,0}

第四次冒泡:
p [ 5 ] = { 1 , 2 , 3 , 4 , 5 } p[5]= \left\{1,2,3,4,5 \right\} p[5]={1,2,3,4,5}
b e f o r e [ 5 ] = { 0 , 0 , 0 , 0 , 0 } before[5]= \left\{0,0,0,0,0 \right\} before[5]={0,0,0,0,0}
结束

我们采用树状数组差分维护这一操作,sum(i)表示当前数列在第i - 1轮冒泡结束时逆序对的数量,显然当i等于1时sum(i)就是原数列的总逆序对数量

record[i]记录有几个数以这个数结尾的逆序对数量为i,以 b e f o r e [ 10 ] = { 0 , 1 , 0 , 3 , 4 , 2 , 5 , 3 , 0 , 1 } before[10]=\left\{0,1,0,3,4,2,5,3,0,1\right\} before[10]={0,1,0,3,4,2,5,3,0,1}为例,before数组记录的就是以每个数结尾的逆序对数量,由图得record[0] = 3,record[1] = 2,record[2] = 1,record[3] = 2,···以此类推

再结合上图冒泡排序过程可以发现第一轮冒泡逆序对数量与原序列的总逆序对的差为n-record[0]=7,第二轮差为n-(record[0]+record[1])=5,第三轮差为n-(record[0]+record[1]+record[3])=4···以此类推
注意这里指的是差,因为我们树状数组维护的其实是差分,也就是说如果要求第c轮冒泡后逆序对数量,只要求sum(c+1)即可

每个before[i]可以看出在每轮冒泡中都会减1,所以只能减到第before[i]轮冒泡,第before[i] + 1轮冒泡就不能减了,每次交换操作只会对两个数的before有影响,根据这个影响去更新差分就好了,具体见代码

100分代码:
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 2e5 + 10;

int p[N];
int before[N];
int record[N];
int n, m;
LL tr[N];

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

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

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

int main()
{
	scanf("%d%d", &n, &m);
	LL tot = 0; 
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &p[i]);
		before[i] = i - sum(p[i]); 
		tot += before[i];			  
		record[before[i]] ++ ;		   
		add(p[i], 1);			
	}
	
	add(1, tot); //差分数组第一项,表示原数列的总逆序对数量
	memset(tr, 0, sizeof tr); //清空,为之后做准备
	tot = 0;  //清空,为之后做准备
	
	for (int i = 0; i < n; i ++ )
	{
		tot += record[i]; 
		add(i + 2, -(n - tot)); 
		//由于下标问题,i必须+2,这样当i=0时就会储存在第2位,而第1位是存总逆序对数量的
	}
	
	while (m -- )
	{
	    int op, c;
		scanf("%d%d", &op, &c);
		if (op == 1)
		{
			c--; //下标从0开始
			if (p[c] < p[c + 1])
			{
				swap(p[c], p[c + 1]);
				swap(before[c], before[c + 1]);
				add(1, 1);	//逆序对总数量+1
				add(before[c + 1] + 2, -1); //根据上文模拟一下
				before[c + 1] ++ ;		
			}
			else if(p[c] > p[c + 1])
			{
				swap(p[c], p[c + 1]);
				swap(before[c], before[c + 1]);
				add(1, -1);	 //逆序对总数量-1
				add(before[c] + 1, 1); //根据上文模拟一下
			    before[c] -- ;
			}
		}
		else
		{
		    c = min(c, n - 1); //c有可能比n-1大,但冒泡排序执行n-1次之后就不会再变了
		    printf("%lld\n", sum(c + 1)); 
		}
		
	}
	
	return 0;
}

P1429. 最小环

题目:最小环
思路:(数学 + 两重贪心)

叕是数学结论题啊啊啊啊!!!
前置知识:
可以先证明出有gcd(n, k)个环(数学太菜,先不证了),每个环上的数的个数为 n g c d ( n , k ) \frac{n}{gcd(n, k)} gcd(n,k)n,并且当环的个数,也就是gcd(n, k)相等时,答案也一定相等,所以可以记忆化每个答案来优化时间复杂度,下面每对引号表示一个环

首先先拿样例来找规律:
k=1时,ans= “ 1 ∗ 2 “\bm1\bm*\bm2 12 + 1 ∗ 3 1*3 13 + 2 ∗ 4 2*4 24 + 3 ∗ 5 3*5 35 + 4 ∗ 6 4*6 46 + 5 ∗ 6 ” \bm5\bm*\bm6” 56
k=2时,ans= “ 1 ∗ 2 “\bm1\bm*\bm2 12 + 1 ∗ 3 1*3 13 + 2 ∗ 3 ” + “ 4 ∗ 5 \bm2\bm*\bm3”+“\bm4\bm*\bm5 23+45 + 4 ∗ 6 4*6 46 + 5 ∗ 6 ” \bm5\bm*\bm6” 56
k=3时,ans= “ 1 ∗ 2 “\bm1\bm*\bm2 12 + 1 ∗ 2 ” + “ 3 ∗ 4 \bm1\bm*\bm2”+“\bm3\bm*\bm4 12+34 + 3 ∗ 4 ” + “ 5 ∗ 6 \bm3\bm*\bm4”+“\bm5\bm*\bm6 34+56+ 5 ∗ 6 ” \bm5\bm*\bm6” 56
不难发现要使总乘积大,则大的跟大的乘,小的自然就跟小的乘,并且还可以发现每个环中,除了最边上两对加粗的数,中间的所有数都差2,这是一个很重要的规律,接下来介绍为什么有这种规律:

  1. 第一重贪心:
    已知几个数和一定,差小积大,所以要让大的数跟大的乘,小的自然就跟小的乘,这样才能实现积的不平均,记每个环上数的个数为p,那么第1大到第p大的放在一个环中,第p+1大到第2p大的放在一个环中,······以此类推,所以先把原数组排个序即可

  2. 第二重贪心:
    在每个环中,我们又要尽量将大的数放在一起,比如样例k=1时,就要把4,5安排在6的旁边,然后再把3安排在5的旁边,2安排在4的旁边,1安排在3的旁边,这样也就有了上面那三个式子,就可以直接做了

100分代码:
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 2e5 + 10; 

LL a[N], f[N]; //f为记忆化数组 
int n, m, k;

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

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i ++ ) scanf("%lld", &a[i]);
	sort(a + 1, a + 1 + n);
	
	while (m -- )
	{
	    LL ans = 0; //乘积会很大
		scanf("%d", &k);
		
		if (k == 0 || n == 1) //特判一下,不然有bug
		{
		  for (int i = 1; i <= n; i ++ )  
		    ans += a[i] * a[i];
		  printf("%lld\n", ans);
		  continue;
		}
		
		int t = gcd(n, k); //环的个数 
		int p = n / t; //每个环上数的个数 
		
		if (f[t]) //以前算过
		{
			printf("%lld\n", f[t]);
			continue;
		}
		
		for (int i = 1; i <= n; i += p)
		{
		    for (int j = 0; j < p - 2; j ++ ) //因为i + j + 2 < i + p,所以j < p - 2 
				ans += a[i + j] * a[i + j + 2];	//规律
				
		    ans += a[i] * a[i + 1] + a[i + p - 2] * a[i + p - 1]; //特殊处理上文中每个环最边上的两对数 
		}
		
		printf("%lld\n", ans);
		f[t] = ans; //记忆化答案 
		
	}
	
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值