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.耐心推公式
这题不难,关键是要理清逻辑关系
我们先固定出谁先手谁后手,那么就可以知道每个人作为先手发球了几轮
默认,发球的都胜利,那么一共 n n n轮
先手发球了 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n⌉ 也赢了 ⌈ n 2 ⌉ \lceil \frac{n}{2} \rceil ⌈2n⌉ 题目中所给条件赢了 a a a
后手发球了 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋ 也赢了 ⌊ n 2 ⌋ \lfloor \frac{n}{2} \rfloor ⌊2n⌋ 题目中所给条件赢了 b b b
假设先手发球输了 x x x场,后手发球输了 y y y场
那么 ⌈ n 2 ⌉ − x + y = a \lceil \frac{n}{2} \rceil-x+y = a ⌈2n⌉−x+y=a
且 ⌊ n 2 ⌋ − y + x = b \lfloor \frac{n}{2} \rfloor -y +x = b ⌊2n⌋−y+x=b
其中 0 ≤ x ≤ ⌈ n 2 ⌉ 0 \le x \le \lceil \frac{n}{2} \rceil 0≤x≤⌈2n⌉ 0 ≤ y ≤ ⌊ n 2 ⌋ 0\le y\le \lfloor \frac{n}{2} \rfloor 0≤y≤⌊2n⌋
消元法,消去 y y y,然后求出 x x x的取值范围
最后交换一下先后手再统计一次答案即可
#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.贪心
简单的贪心问题
首先单看每一组,我们可以统计出攻略这个组中所有的怪兽所需要最小力量
即 m a x ( a i − i ) + 1 max(a_i-i)+1 max(ai−i)+1 其中 0 ≤ i < l e n 0\le i< len 0≤i<len记本组长度为 l e n len len
那么我们现在知道了每一组的最小攻略力量和每一组的怪兽数量
要处理的是将那些组放在前面攻略,将那些组放在后面攻略 理所当然被放在后面的组在攻略时更占优势
策略是贪心,我们按照攻略的难易程度,由易到难进行排序,然后统计出答案即可。
为什么贪心是对的呢?
我们不妨设想,假设最终我们是在攻略第 i i i组时导致需要使用的力量最高
那么,如果将他和一个攻略难度更高的进行交换,事情只会变得更糟糕!
#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.整除分块
总体策略是 d p dp dp
d p [ i ] dp[i] dp[i]表示 n = i n=i n=i时的答案
很明显根据第一个操作, d p [ i ] dp[i] dp[i]可以转移到 d p [ 1 ] , d p [ 2 ] , d p [ 3 ] , d p [ 4 ] … d p [ i − 1 ] dp[1],dp[2],dp[3],dp[4]\ldots dp[i-1] dp[1],dp[2],dp[3],dp[4]…dp[i−1]
这个我们维护一个前缀和 p r e [ i ] pre[i] pre[i]即可 d p [ i ] + = p r e [ i − 1 ] dp[i]+=pre[i-1] dp[i]+=pre[i−1]
关键是第二个条件,我们无法利用前缀和加快状态的转移
但是我们注意到了,他所转移的状态好像不是很多
这便是经典的整除分快了
对于 l ≤ n l\leq n l≤n 整除结果为 ⌊ n l ⌋ \lfloor \frac{n}{l} \rfloor ⌊ln⌋
那么对于 r = ⌊ n ⌊ n l ⌋ ⌋ r = \lfloor \frac{n}{\lfloor \frac{n}{l} \rfloor} \rfloor r=⌊⌊ln⌋n⌋
⌊ n r ⌋ = ⌊ n l ⌋ \lfloor \frac{n}{r} \rfloor = \lfloor \frac{n}{l} \rfloor ⌊rn⌋=⌊ln⌋ 且 ⌊ n r + 1 ⌋ ≠ ⌊ n l ⌋ \lfloor \frac{n}{r+1} \rfloor \ne \lfloor \frac{n}{l} \rfloor ⌊r+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.思维
O K OK OK n n n的范围变成了 4 e 6 4e6 4e6很明显, O ( n n ) O(n\sqrt{n}) O(nn)的算法不再起作用
我们需要更快的算法!!
关键还是在于如何提高操作 2 2 2的转移速度!!!
这次我们反过来思考,似乎从 d p [ i ] dp[i] dp[i]向 d p [ j ] , j < i dp[j],j<i dp[j],j<i 的转移 O ( n n ) O(n\sqrt n) O(nn)已经到极限了
我们考虑,在 d p [ i ] dp[i] dp[i]已经求出的情况下,向 d p [ j ] , j > i dp[j],j>i dp[j],j>i更新 这也是 d p dp dp的一个常见的想法
为此我们需要仔细地思考操作二的意义
对于 j j j, 2 ≤ z ≤ j 2 \le z \le j 2≤z≤j ⌊ j z ⌋ = i \lfloor \frac{j}{z} \rfloor = i ⌊zj⌋=i
那么对于 [ z i , z i + z − 1 ] [zi,zi+z-1] [zi,zi+z−1] 中的所有数 j j j ⌊ j z ⌋ = i \lfloor \frac{j}{z} \rfloor = i ⌊zj⌋=i
因此,当我们求出 d p [ i ] dp[i] dp[i]的情况下,我们可以枚举 2 ≤ z ≤ n 2 \le z\le n 2≤z≤n
对每一个 [ z i , z i + z − 1 ] [zi,zi+z-1] [zi,zi+z−1] 区间加上 d p [ i ] dp[i] dp[i]
从而实现操作二!!!
暂时不考虑区间加的代价,我们发现这是一个调和级数的复杂度 O ( n l o g n ) O(nlogn) O(nlogn)
有了区间加,我们如果使用线段树或者树状数组复杂度上升至 n l o g n 2 nlogn^2 nlogn2
这里我们选择前缀和维护区间加操作即可 总复杂度为 O ( n l o g n ) 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.构造
一步步思考
一开始看感觉很束手无策
我们不妨先减少一些条件,假如没有长度为奇数的限制的话,我们该如何构造呢?
这种构造有一种比较套路的构造方法:一个一个放位置
即,我们先把 n n n放在 a [ n ] a[n] a[n]上,再把 n − 1 n-1 n−1放在 a [ n − 1 ] a[n-1] a[n−1]上 … \ldots …
这种构造非常简单,我们先找到 n n n在原数组中的位置 i d n id_n idn
然后 r e v e r s e ( i d n ) reverse(id_n) reverse(idn) 然后 r e v e r s e ( n ) reverse(n) reverse(n)
即,我们先找到 i i i在原数组中的位置 i d i id_i idi
然后 r e v e r s e ( i d i ) , r e v e r s e ( i ) reverse(id_i),reverse(i) reverse(idi),reverse(i)
加上奇数的限制的话该怎么办呢?
奇数限制改变了什么呢?
我们猛然发现对于一个数 a a a她在奇数位置上,那么无论 r e v e r s e reverse reverse多少次,她最终还是还会在奇数位置上
如果 a a a在偶数位置上,最终她还是在偶数位置上
换而言之,奇数位置的数是绝对无法和偶数位置上的数交换的!
也就是说,奇数位置上必须要有 1 , 3 , 5 , 7 … n 1,3,5,7\ldots n 1,3,5,7…n
偶数位置上必须要有 2 , 4 , 6 , … n − 1 2,4,6,\dots n-1 2,4,6,…n−1
然后
我们可以按照简化版的方案将奇数位置上的数排序
我们也可以模仿按照简化版的方案将偶数位置上的数排序
但是现实是骨感的,我们对奇数位置上的数排序后,再对偶数位置上的数排序时
会打乱奇数位置上的有序状态
很糟糕!
因此,我们决定将奇数和偶数绑定在一起排序
即,我们第一次将 n , n − 1 n,n-1 n,n−1归位,再将 n − 2 , n − 3 n-2,n-3 n−2,n−3归位 … \ldots …
即,我们将 i , i − 1 ( i i s o d d ) i,i-1 (i\ is\ odd) i,i−1(i is odd)绑定在一起,使 i d i = i d i − 1 + 1 id_i=id_{i-1}+1 idi=idi−1+1
我们 r e v e r s e ( i d i ) , r e v e r s e ( i ) reverse(id_i),reverse(i) reverse(idi),reverse(i)即可
这样的话,关键就是绑定操作了!
如何绑定呢?
我们可以先找到 i d i id_i idi 然后 r e v e r s e ( i d i ) reverse(id_i) reverse(idi)
此时 i d i = 1 id_i=1 idi=1我们再 r e v e r s e ( i d i − 1 − 1 ) reverse(id_{i-1}-1) reverse(idi−1−1)
此时 i d i = i d i − 1 − 1 id_i=id_{i-1}-1 idi=idi−1−1
我们再 r e v e r s e ( i d i − 1 + 1 ) reverse(id_{i-1}+1) reverse(idi−1+1)
此时 i d i = 3 , i d i − 1 = 2 id_i=3,id_{i-1}=2 idi=3,idi−1=2
r e v e r s e ( 3 ) reverse(3) reverse(3) i d i = 1 , i d i − 1 = 2 id_i=1,id_{i-1}=2 idi=1,idi−1=2 成功绑定了
最后再 r e v e r s e ( i ) reverse(i) reverse(i)即可
也就是说,我们想要归位 i − 1 , i i-1,i i−1,i ( i i i为奇数) 需要的操作为
r e v e r s e ( i d i ) reverse(id_i) reverse(idi)
r e v e r s e ( i d i − 1 − 1 ) reverse(id_{i-1}-1) reverse(idi−1−1)
r e v e r s e ( i d i − 1 + 1 ) reverse(id_{i-1}+1) reverse(idi−1+1)
r e v e r s e ( 3 ) reverse(3) reverse(3)
r e v e r s e ( i ) reverse(i) reverse(i)
正好 5 5 5步!!!
问题解决!!
#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";
}
}