Codeforces Round #740 (Div. 2) CF1561

本文介绍了四种不同的编程解决方案:模拟暴力法处理小规模数据,通过耐心推导公式计算最优策略,采用贪心算法优化怪兽攻略顺序,以及利用整除分块和区间加法优化复杂度。详细解析了每种方法的应用场景和逻辑过程。

A.模拟

看一下,数据范围很小,我们按照题意模拟暴力就好了

#include<bits/stdc++.h>
using namespace std;
int a[950];
int n;
inline bool check()
{
	for (int i=1;i<=n;++i)if (a[i]!=i)return false;
	return true;
}
int main()
{
	ios::sync_with_stdio(0);
	int t;cin>>t;
	while (t--)
	{
		cin>>n;
		for (int i=1;i<=n;++i)cin>>a[i];
		int ans=0;
		while (!check())
		{
			++ans;
			if (ans&1)
			{
				for (int i=1;i<=n-2;i+=2)if (a[i]>a[i+1])swap(a[i],a[i+1]);
			}
			else
			{
				for (int i=2;i<=n-1;i+=2)if (a[i]>a[i+1])swap(a[i],a[i+1]);
			}
		}cout<<ans<<"\n";
	}
}

B.耐心推公式

这题不难,关键是要理清逻辑关系

我们先固定出谁先手谁后手,那么就可以知道每个人作为先手发球了几轮

默认,发球的都胜利,那么一共nnn

先手发球了⌈n2⌉\lceil \frac{n}{2} \rceil2n 也赢了⌈n2⌉\lceil \frac{n}{2} \rceil2n 题目中所给条件赢了aaa

后手发球了⌊n2⌋\lfloor \frac{n}{2} \rfloor2n 也赢了⌊n2⌋\lfloor \frac{n}{2} \rfloor2n 题目中所给条件赢了bbb

假设先手发球输了xxx场,后手发球输了yyy

那么⌈n2⌉−x+y=a\lceil \frac{n}{2} \rceil-x+y = a2nx+y=a

⌊n2⌋−y+x=b\lfloor \frac{n}{2} \rfloor -y +x = b2ny+x=b

其中0≤x≤⌈n2⌉0 \le x \le \lceil \frac{n}{2} \rceil0x2n 0≤y≤⌊n2⌋0\le y\le \lfloor \frac{n}{2} \rfloor0y2n

消元法,消去yyy,然后求出xxx的取值范围

最后交换一下先后手再统计一次答案即可

#include<bits/stdc++.h>
using namespace std;
set<int> se;

//na-x+y=a,nb-y+x=b;
//x-y=na-a=b-nb
//y=x-na+a
//0<=x<=na,0<=y<=nb
//0<=x<=na,na-a<=x<=nb+na-a
void solve(int a,int b)
{
	int sum = a+b;
	int na = (sum+1)/2;
	int nb = sum-na;
	for (int i=max(0,na-a);i<=min(na,b);++i)se.insert(i+i-na+a);
	
}
int main()
{
	ios::sync_with_stdio(0);
	int t;cin>>t;
	while (t--)
	{
		se.clear();
		int a,b;
		cin>>a>>b;
		solve(a,b);
		solve(b,a);
		cout<<(int)se.size()<<endl;
		for (int res:se)cout<<res<<" ";
		cout<<endl;
	}
}

C.贪心

简单的贪心问题

首先单看每一组,我们可以统计出攻略这个组中所有的怪兽所需要最小力量

max(ai−i)+1max(a_i-i)+1max(aii)+1 其中0≤i<len0\le i< len0i<len记本组长度为lenlenlen

那么我们现在知道了每一组的最小攻略力量和每一组的怪兽数量

要处理的是将那些组放在前面攻略,将那些组放在后面攻略 理所当然被放在后面的组在攻略时更占优势

策略是贪心,我们按照攻略的难易程度,由易到难进行排序,然后统计出答案即可。

为什么贪心是对的呢?

我们不妨设想,假设最终我们是在攻略第iii组时导致需要使用的力量最高

那么,如果将他和一个攻略难度更高的进行交换,事情只会变得更糟糕!

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e5+100;
int n;
pair<int,int> res[maxn];
int main()
{
	ios::sync_with_stdio(0);
	int t;cin>>t;
	while (t--)
	{
		cin>>n;
		for (int i=1;i<=n;++i)
		{
			cin>>res[i].second;
			res[i].first=0;
			for (int j=0;j<res[i].second;++j)
			{
				int num;cin>>num;
				res[i].first = max(res[i].first,num-j);
			}
		}
		sort(res+1,res+1+n);
		int pre=0;
		int ans=0;
		for (int i=1;i<=n;i++)
		{
			res[i].first -= pre;
			ans = max(ans,res[i].first);
			pre += res[i].second;
		}
		cout<<ans+1<<endl;
	}
}

D1.整除分块

总体策略是dpdpdp

dp[i]dp[i]dp[i]表示 n=in=in=i时的答案

很明显根据第一个操作,dp[i]dp[i]dp[i]可以转移到dp[1],dp[2],dp[3],dp[4]…dp[i−1]dp[1],dp[2],dp[3],dp[4]\ldots dp[i-1]dp[1],dp[2],dp[3],dp[4]dp[i1]

这个我们维护一个前缀和pre[i]pre[i]pre[i]即可 dp[i]+=pre[i−1]dp[i]+=pre[i-1]dp[i]+=pre[i1]

关键是第二个条件,我们无法利用前缀和加快状态的转移

但是我们注意到了,他所转移的状态好像不是很多

这便是经典的整除分快了

对于l≤nl\leq nln 整除结果为⌊nl⌋\lfloor \frac{n}{l} \rfloorln

那么对于r=⌊n⌊nl⌋⌋r = \lfloor \frac{n}{\lfloor \frac{n}{l} \rfloor} \rfloorr=lnn

⌊nr⌋=⌊nl⌋\lfloor \frac{n}{r} \rfloor = \lfloor \frac{n}{l} \rfloorrn=ln⌊nr+1⌋≠⌊nl⌋\lfloor \frac{n}{r+1} \rfloor \ne \lfloor \frac{n}{l} \rfloorr+1n=ln

依据这个结论我们可以快速地跳跃找到所有的状态

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 2e5+100;
ll mod,n;
ll dp[maxn];
ll pre[maxn];

int main()
{
	ios::sync_with_stdio(0);
	cin>>n>>mod;
	dp[1]=pre[1]=1;
	for (int i=2;i<=n;++i)
	{
		dp[i]=pre[i-1];
		int l=2;
		while (l<=i)
		{
			int r = i/(i/l);
			dp[i] = (dp[i]+(r-l+1)*dp[i/l]%mod)%mod;
			l = r+1;
		}
		pre[i]=(pre[i-1]+dp[i])%mod;
//		cout<<i<<":"<<dp[i]<<","<<pre[i]<<endl;
	}
	cout<<dp[n]<<endl;
}

D2.思维

OKOKOK nnn的范围变成了4e64e64e6很明显,O(nn)O(n\sqrt{n})O(nn)的算法不再起作用

我们需要更快的算法!!

关键还是在于如何提高操作222的转移速度!!!

这次我们反过来思考,似乎从dp[i]dp[i]dp[i]dp[j],j<idp[j],j<idp[j],j<i 的转移O(nn)O(n\sqrt n)O(nn)已经到极限了

我们考虑,在dp[i]dp[i]dp[i]已经求出的情况下,向dp[j],j>idp[j],j>idp[j],j>i更新 这也是dpdpdp的一个常见的想法

为此我们需要仔细地思考操作二的意义

对于jjj,2≤z≤j2 \le z \le j2zj ⌊jz⌋=i\lfloor \frac{j}{z} \rfloor = izj=i

那么对于[zi,zi+z−1][zi,zi+z-1][zi,zi+z1] 中的所有数jjj ⌊jz⌋=i\lfloor \frac{j}{z} \rfloor = izj=i

因此,当我们求出dp[i]dp[i]dp[i]的情况下,我们可以枚举2≤z≤n2 \le z\le n2zn

对每一个[zi,zi+z−1][zi,zi+z-1][zi,zi+z1] 区间加上dp[i]dp[i]dp[i]

从而实现操作二!!!

暂时不考虑区间加的代价,我们发现这是一个调和级数的复杂度O(nlogn)O(nlogn)O(nlogn)

有了区间加,我们如果使用线段树或者树状数组复杂度上升至nlogn2nlogn^2nlogn2

这里我们选择前缀和维护区间加操作即可 总复杂度为O(nlogn)O(nlogn)O(nlogn)

速度还是很可观的

#include<bits/stdc++.h>
using namespace std;
const int maxn = 4e6+100;
int n,mod;
int pre[maxn];//与D1不同,这里维护的是操作二的前缀和
int main()
{
	ios::sync_with_stdio(0);
	cin>>n>>mod;
	int tmp = 0,ans = 0;//tmp 为D1中的pre
	for (int i=1;i<=n;++i)
	{
		pre[i] = (pre[i]+pre[i-1])%mod;
		ans = (pre[i]+tmp)%mod;
		if (i==1)ans=1;
		tmp = (tmp+ans)%mod;
		
		for (int j=2;j<=n;++j)
		{
			int l = i*j;
			int r = (i+1)*j-1;
			r = min(r,n);
			if (l>n)break;
			pre[l]=(pre[l]+ans)%mod;
			pre[r+1]=(pre[r+1]-ans+mod)%mod;
		}
	}
	cout<<ans<<endl;
}

E.构造

一步步思考

一开始看感觉很束手无策

我们不妨先减少一些条件,假如没有长度为奇数的限制的话,我们该如何构造呢?

这种构造有一种比较套路的构造方法:一个一个放位置

即,我们先把nnn放在a[n]a[n]a[n]上,再把n−1n-1n1放在a[n−1]a[n-1]a[n1]…\ldots

这种构造非常简单,我们先找到nnn在原数组中的位置idnid_nidn

然后reverse(idn)reverse(id_n)reverse(idn) 然后reverse(n)reverse(n)reverse(n)

即,我们先找到iii在原数组中的位置idiid_iidi

然后 reverse(idi),reverse(i)reverse(id_i),reverse(i)reverse(idi),reverse(i)

加上奇数的限制的话该怎么办呢?

奇数限制改变了什么呢?

我们猛然发现对于一个数aaa她在奇数位置上,那么无论reversereversereverse多少次,她最终还是还会在奇数位置上

如果aaa在偶数位置上,最终她还是在偶数位置上

换而言之,奇数位置的数是绝对无法和偶数位置上的数交换的!

也就是说,奇数位置上必须要有1,3,5,7…n1,3,5,7\ldots n1,3,5,7n

偶数位置上必须要有2,4,6,…n−12,4,6,\dots n-12,4,6,n1

然后

我们可以按照简化版的方案将奇数位置上的数排序

我们也可以模仿按照简化版的方案将偶数位置上的数排序

但是现实是骨感的,我们对奇数位置上的数排序后,再对偶数位置上的数排序时

会打乱奇数位置上的有序状态

很糟糕!

因此,我们决定将奇数和偶数绑定在一起排序

即,我们第一次将n,n−1n,n-1n,n1归位,再将n−2,n−3n-2,n-3n2,n3归位 …\ldots

即,我们将i,i−1(i is odd)i,i-1 (i\ is\ odd)i,i1(i is odd)绑定在一起,使idi=idi−1+1id_i=id_{i-1}+1idi=idi1+1

我们reverse(idi),reverse(i)reverse(id_i),reverse(i)reverse(idi),reverse(i)即可

这样的话,关键就是绑定操作了!

如何绑定呢?

我们可以先找到idiid_iidi 然后reverse(idi)reverse(id_i)reverse(idi)

此时idi=1id_i=1idi=1我们再reverse(idi−1−1)reverse(id_{i-1}-1)reverse(idi11)

此时idi=idi−1−1id_i=id_{i-1}-1idi=idi11

我们再reverse(idi−1+1)reverse(id_{i-1}+1)reverse(idi1+1)

此时idi=3,idi−1=2id_i=3,id_{i-1}=2idi=3,idi1=2

reverse(3)reverse(3)reverse(3) idi=1,idi−1=2id_i=1,id_{i-1}=2idi=1,idi1=2 成功绑定了

最后再reverse(i)reverse(i)reverse(i)即可

也就是说,我们想要归位i−1,ii-1,ii1,iiii为奇数) 需要的操作为

reverse(idi)reverse(id_i)reverse(idi)

reverse(idi−1−1)reverse(id_{i-1}-1)reverse(idi11)

reverse(idi−1+1)reverse(id_{i-1}+1)reverse(idi1+1)

reverse(3)reverse(3)reverse(3)

reverse(i)reverse(i)reverse(i)

正好555步!!!

问题解决!!

#include<bits/stdc++.h>
using namespace std;
int a[2077];
int n;
inline int find_id(int tar)
{
	for (int i=1;i<=n;++i)if (a[i]==tar)return i;
}
vector<int> res;
int main()
{
	ios::sync_with_stdio(0);
	int t;cin>>t;
	while (t--)
	{

		cin>>n;res.clear();
		for (int i=1;i<=n;++i)cin>>a[i];
		if (n==1)
		{
			cout<<"0\n";
			continue;
		}
		bool f = true;
		for (int i=n;i>1;i-=2)
		{
			int id1 = find_id(i);
			int id2 = find_id(i-1);
			if (id1%2==0||id2%2==1)
			{
				f=false;
				cout<<"-1\n";
				break;
			}
			
			res.push_back(id1);
			reverse(a+1,a+1+id1);
			id1 = 1;
			id2 = find_id(i-1);
			
			res.push_back(id2-1);
			reverse(a+1,a+1+id2-1);
			id1 = id2-1;
			
			res.push_back(id2+1);
			reverse(a+1,a+1+id2+1);
			id1=3;id2=2;
			
			res.push_back(3);
			reverse(a+1,a+1+3);
			id1=1;id2=2;
			
			res.push_back(i);
			reverse(a+1,a+1+i);
			
		}if (!f)continue;
		
		cout<<res.size()<<"\n";
		for (int num:res)cout<<num<<" ";
		cout<<"\n";
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值