CUSTACM Summer Camp 2022 Training 3
A. Fillomino 2
题意
n*n的网格上,把1到n的数放在对角线上,要求把对角线以下(包括对角线)分成n个区域
- 每个区域都是相连的
- 第x个区域必须包含对角线上填有x的网格
- 第x个区域有包含x格网格,并且网格中数字都为x
给你n的大小以及对角线上数字的分布,问是否可以划分为符合以上条件的n个区域,如果能输出划分结果否则输出-1
数据范围: 1 ≤ n ≤ 500 1 \leq n \leq 500 1≤n≤500
tags:思维
思路
结果是一定可以划分
因为我们可以从最左上角开始划分,而且可以保证该划分不会影响后面的划分
- 如果能填左边就填左边
- 否则填下面
- 再否则填右边
因为按照这样划分是对整个区域(对角线以下的区域)外层填充,每次都从外圈一点点把里面区域填满,前面的划分自然不会影响到后面的划分
代码
#include<iostream>
using namespace std;
const int maxn=505;
int n;
int a[maxn][maxn];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
int x;cin>>x;
a[i][i]=x;
}
for(int i=1;i<=n;i++){
int x=i,y=i,t=a[i][i];
for(int j=0;j<t-1;j++){//填t-1次数字a[i][i]
if(a[x][y-1]==0&&y-1>=1){//能填左边就填左边
a[x][--y]=t;
continue;
}
else if(a[x+1][y]==0&&x+1<=n){//不能填左边就填下面
a[++x][y]=t;
continue;
}
else if(a[x][y+1]==0){//不能填左边和下面就往右边填
a[x][++y]=t;
continue;
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cout<<a[i][j]<<' ';
}
cout<<endl;
}
}
复杂度
O ( n 2 ) O(n^2) O(n2)
B. AGAGA XOOORRR
题意
给你n个数字,可以把任意相邻的两个数字a、b换成a^b,问能否是这把其变为一串数字都相等的序列(该序列至少要有两个数)
数据大小: 1 ≤ t ≤ 15 , 2 ≤ n ≤ 2000 , 0 ≤ a i ≤ 2 30 1 \leq t \leq 15,2 \leq n \leq 2000,0 \leq a_i \leq 2^{30} 1≤t≤15,2≤n≤2000,0≤ai≤230
tags:位运算
思路
如果能变成数字都相等(为x)的序列,这些数字在继续执行两两异或操作,只会有两种可能
- 序列长度为偶数,最终结果为0
- 序列长度为奇数,最终为x
因为异或运算与顺序无关!!,我们就可以从把开始的n个数字做异或和
- 若结果为0,那么一定可以达到上面目的(只有达到”序列长度为偶数且数字都相等“这个中间状态,才会使异或和为0)
- 若结果为一个数y,看这些异或和是否可以组成 ≥ 2 \geq2 ≥2个y即可(要最后更具y的值判断是否可以达到”序列长度为奇数且异或和为y“这个状态)
代码
#include<iostream>
#define ll long long
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
int n;
cin>>n;
int a[2005];
ll x=0;
for(int i=0;i<n;i++){
cin>>a[i];
x^=a[i];//异或和
}
if(!x)cout<<"YES"<<endl;//若异或和为0,则满足条件
else{//若异或和非零,若想满足条件,则需让n个数异或成大于等于2个x(异或和)
ll count=0,y=0;
for(int i=0;i<n;i++){
y^=a[i];
if(y==x){//异或到=x,就重新开始奇数
count++;
y=0;
}
}
if(y==0&&count>=2)cout<<"YES"<<endl;//要最后判断以下最后一次异或是否可以得到x,也就是y会不会变为0
else cout<<"NO"<<endl;
}
}
}
复杂度
O ( n ) O(n) O(n)
思维
很多关于异或的题目突破口都是异或与顺序无关来就异或和,用异或和作为突破口
C. Phoenix and Socks
题意
有n个袜子,有l个左袜子和r个右袜子(l+r=n),每个袜子都有颜色,有三种操作
- 改变其颜色为任意一种
- 把左袜子变为右袜子,不改颜色
- 把右袜子变为左袜子,不改颜色
问得到n/2双左右和颜色匹配的袜子的最小操作数
数据范围: 2 ≤ n ≤ 2 ∗ 1 0 5 2 \leq n \leq 2*10^5 2≤n≤2∗105
tags:贪心
思路
- 能匹配好的袜子先匹配,并剩下lx个左袜子,rx个右袜子
- 如果lx=rx,就只能一个个改颜色来匹配
- 如果lx!=rx,从袜子多(加上左袜子多)的哪一堆自己匹配(把颜色相同的数量大于2的左袜子中其中一个变为右袜子),直到不能匹配或者达到lx=rx为止
- 若是不能匹配为止,就得翻转袜子使lx=rx了,然后在一个个改颜色
- 若达到lx=rx为止,就一个个改颜色
代码
#include<iostream>
#include<cmath>
using namespace std;
const int maxn=2e5+5;
int n,l,r;
int c[maxn],ll[maxn],rr[maxn];
void dif(){
for(int i=1;i<=n;i++){
while(ll[c[i]]>0&&rr[c[i]]>0)ll[c[i]]--,rr[c[i]]--,l--,r--;
}
}
int self(int*x){
int count=0;
for(int i=1;i<=n;i++){
while(x[c[i]]>=2){
x[c[i]]-=2;
count++;
}
}
return count;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
cin>>n>>l>>r;
for(int i=1;i<=n;i++)ll[i]=rr[i]=0;//多组输入注意初始化
for(int i=1;i<=n;i++){
cin>>c[i];
if(i<=l)ll[c[i]]++;//记录左袜子颜色为c[i]的数量
else rr[c[i]]++;//记录右袜子颜色为c[i]的数量
}
dif();//先把可以匹配的匹配完
int lc=self(ll),rc=self(rr);//在计算左袜子和右袜子各自可以形成多少次自匹配
if(l>r){
int x=(l-r)/2;//计算需要多少次自匹配
if(lc>=x){
cout<<x+r<<endl;//如果可执行的自匹配数>需要的自匹配数,就执行x+l次(自匹配需要翻转的次数+一次次改颜色的次数)
}
else{
cout<<x+(x-lc)+r<<endl;//否则就要多执行(x-lc)次改颜色
}
}
else if(l<r){
int x=(r-l)/2;
if(rc>=x){
cout<<x+l<<endl;
}
else {
cout<<x+(x-rc)+l<<endl;
}
}
else cout<<l<<endl;//如果l=r,就只用执行一个个改颜色l次
}
}
复杂度
O ( n ) O(n) O(n)
D. Berland Regional
题意
有n个人,第i个人的属于ui,成绩为ai。
举办比赛,每轮比赛要每个学校出成绩高的前k个人组成一队,举行到没有学校能出一队为止
参加比赛的所有人的总成绩为x。
输出k分别为1到n时的x
数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 , 1 ≤ a i ≤ 1 0 9 1 \leq n \leq 2*10^5,1 \leq a_i \leq 10^9 1≤n≤2∗105,1≤ai≤109
tags:前缀和
思路
利用vector把学生划分到各个学校中,然后将其成绩排序,在求好前缀和
对于k,每个学校(由x人)的贡献为sum[x/k*k-1]也就是前x/k *k个人的成绩总和嘛
注意:不要外层循环去枚举k里层循环去枚举n,这样的复杂度为n2了,而且因为k很大的时候会由很多一个学校出不了队伍而导致n的循环多余
代码
#include<iostream>
#include<algorithm>
#include<vector>
#define ll long long
using namespace std;
const int maxn=2e5+5;
int n;
int a[maxn],b[maxn];
vector<ll>u[maxn];
ll ans[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
cin>>n;
for(int i=1;i<=n;i++){
u[i].clear();
ans[i]=0;
}
for(int i=0;i<n;i++)cin>>a[i];
for(int i=0;i<n;i++)cin>>b[i];
for(int i=0;i<n;i++)u[a[i]].push_back(b[i]);//学生归类
for(int i=1;i<=n;i++){
if(!u[i].size())continue;
sort(u[i].begin(),u[i].end(),greater<ll>());//从小到大排序
for(int j=1;j<u[i].size();j++)u[i][j]+=u[i][j-1];//求前缀和
}
for(int i=1;i<=n;i++){
int len=u[i].size();
for(int k=1;k<=len;k++){//只有枚举k到学校人数,当k大于学校人数时该学校出不了人,a[k]的贡献为0
ans[k]+=u[i][len/k*k-1];
}
}
for(int i=1;i<=n;i++)cout<<ans[i]<<' ';
cout<<endl;
}
}
复杂度
O ( n ) O(n) O(n)
E. Restoring the Permutation
题意
给一数组,求原数组,给定的数组的第i位表示原数组1到i的最大值。
输出可能的原数组的最小字典序和最大字典序。数据范围: 1 ≤ n ≤ 2 ∗ 1 0 5 1 \leq n \leq 2*10^5 1≤n≤2∗105
tags:二分,思维
思路
- 第一次出现的数一定是固定的,我们只需去填那些非固定的数就行了
- 对于最小字典序,把未固定数排序,最小的数依次放到空位置就行,不会影响该数组结构(因为每次都是放最小的数)
- 但是对于最大字典序,如果把大的数放在前面,可能会改变数组应有的结构,所以不能满目的把最大的数依次放空位置,而是放比该空位置前固定的数x小的数中(未固定的数中)的最大数(要用二分查找)
代码
#include<iostream>
#include<algorithm>
#include<map>
#include<vector>
using namespace std;
const int maxn=2e5+5;
int n;
int a[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
cin>>n;
int b[maxn]={0};
map<int,int>appear;
vector<int>no;
for(int i=1;i<=n;i++){
cin>>a[i];
if(i!=1&&a[i]!=a[i-1])b[i]=a[i],appear[a[i]]++;//appear是已固定的数
}
b[1]=a[1],appear[a[1]]++;
for(int i=1;i<=n;i++)if(!appear[i])no.push_back(i);//no中是未固定的数
sort(no.begin(),no.end());//从小到大排序
int x=0;
for(int i=1;i<=n;i++){//对于最小字典序,固定的输出,空位置输出no中的最小数
if(b[i])cout<<b[i]<<' ';
else cout<<no[x++]<<' ';
}
cout<<endl;
// sort(no.begin(),no.end(),greater<int>());
x=0;
for(int i=1;i<=n;i++){
if(b[i]){
x=b[i];//固定的直接输出,并保存
cout<<b[i]<<' ';
}
else{
// for(int j=0;j<no.size();j++){
// if(no[j]<x){
// cout<<no[j]<<' ';
// no.erase(no.begin()+j);
// break;
// }
// }
int it=upper_bound(no.begin(),no.end(),x)-no.begin()-1;//用二分!!,no中第一个大于x(空位置前第一个固定的数)后的数一定是<x的数中最大的数
cout<<no[it]<<' ';
no.erase(no.begin()+it);//输出并删除
}
}
cout<<endl;
}
}
复杂度
O ( n l o g n ) O(nlogn) O(nlogn)
F. Planar Reflections动规好题
题意
假设现在有n个平面,从左到右排列,现有一个衰变年龄为k kk的粒子从最左端向右发射
如果一个粒子碰到了一个平面,它将会不改变衰变年龄保持运动方向并传过这个平面
如果这个粒子的衰变年龄大于1,那么将会分裂出一个衰变年龄为k-1的往相反方向运动的粒子
给定n和k,问这个空间最终会有多少个粒子,答案对1e9+7取模
数据范围:1≤T≤100,1≤n,k≤1000,∑n≤1000,∑k≤1000
tags:动态规划
思路
很有意思的动态规划
- 抽象意义:
dp[i][j]
表示前方还有i个屏幕衰变年龄为j粒子对空间中粒子数的贡献 - 边界处理:
- 对于衰败年龄为1的粒子,不管前面有多少个屏幕,它的贡献都为1,
dp[0~n][1] = 1;
- 对于前方屏幕为0的情况,不管粒子衰变年龄为多少,它的贡献都为1,
dp[0][1~k] = 1;
- 对于衰败年龄为1的粒子,不管前面有多少个屏幕,它的贡献都为1,
- 状态转移:
dp[i][j] = (dp[i-1][j] + dp[n-i][j-1]) % mod;
前方有i-1个屏幕衰变年龄为j的贡献+前方有n-i个屏幕衰变数量为j-1的贡献 - 遍历顺序:注意不能直接照搬状态转移方程,因为可以发现在计算
dp[i][j]
时其来源dp[n-1][j-1]
可能还未确定。所以得先对衰变年龄遍历在对屏幕数遍历 - 最终结果:
dp[n][k]
代码
#include <iostream>
#define ll long long
using namespace std;
const int maxn = 1e3 + 5, mod = 1e9 + 7;
int n, k;
ll dp[maxn][maxn];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for (cin >> t; t; t--)
{
cin >> n >> k;
for (int i = 0; i <= n; i++)
dp[i][1] = 1;
for (int i = 1; i <= k; i++)
dp[0][i] = 1;
for (int i = 2; i <= k; i++)
{
for (int j = 1; j <= n; j++)
{
dp[j][i] = (dp[j - 1][i] + dp[n - j][i - 1]) % mod;
}
}
cout << dp[n][k] << endl;
}
}
复杂度
O ( k ∗ n ) O(k*n) O(k∗n)
思维
对于动态规划,不要总是去想它的中间过程的细节,严格按照上面的步骤来分析就行:抽象意义→边界处理→状态转移方程→遍历顺序→最终结果
G. Building a Fence
题意
给了n个宽为1,高为k的栅栏,以及凹凸不平的地,长度为h1,先要求将栅栏放到这地上,要求
- 第一个栅栏和最后一个栅栏只能放到地上
- 中间的栅栏最多可以高于地面k−1的高度放置
- 相邻栅栏至少有高度1的单位是毗邻的
问是否存在放置要求满足以上方案。
数据范围: 2 ≤ n ≤ 2 ∗ 1 0 5 , 2 ≤ k ≤ 1 0 8 , 0 ≤ h i ≤ 1 0 8 2 \leq n \leq 2*10^5,2 \leq k \leq 10^8,0 \leq h_i \leq 10^8 2≤n≤2∗105,2≤k≤108,0≤hi≤108
tags:思维,递推
思路
假设每一个栅栏,其前一个栅栏底部的最底高度为down,最高高度为up。那么对于下一个栅栏的底部:
- 最底高度:
- 肯定会大于地面高度 h i h_i hi
- 与前一个栅栏底部(底部为的最低高度)除只有1米毗邻往下 d o w n 前 − k + 1 down_前-k+1 down前−k+1
- 最高高度:
- 高于地面 k − 1 k-1 k−1
- 与前一个栅栏顶部(底部为最高高度)除只有1米毗邻往上 u p 前 + k − 1 up_前+k-1 up前+k−1
- 当
max(down-k+1,h[i])<=min(up+k-1,h[i]+k-1)
是该栅栏就可放了,并更新down与up
最后要特判一下最后一个栅栏是否可以放在地上
代码
#include<iostream>
#define ll long long
using namespace std;
const int maxn=2e5+5;
int n,k;
int h[maxn];
bool judge(){
int down=h[0],up=h[0];
for(int i=1;i<n-1;i++){
down=max(down-k+1,h[i]);
up=min(up+k-1,h[i]+k-1);
if(down>up)return false;
}
down=down-k+1,up=up+k-1;
return (h[n-1]>=down&&h[n-1]<=up);//特判是否可以放在地上
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
cin>>n>>k;
for(int i=0;i<n;i++)cin>>h[i];
if(judge())cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
}
复杂度
O ( n ) O(n) O(n)
H. Ceil Divisions
题意
数据范围: 3 ≤ n ≤ 2 ∗ 1 0 5 3 \leq n \leq 2*10^5 3≤n≤2∗105
tags:思维
思路
因为**[x/x+1]=1**
所以我们可以从3开始一直执行n-3次此操作,最后剩下n
我们还有8次操作可以使用,但是如果n很大时对n进行8次除2取整并不能使其变为1
但是要知道指数的增长很快的,既然除2不行,那就除8,我们就会剩下6次对n除8取整的操作,可以计算86大于2*105
代码
#include<iostream>
using namespace std;
const int maxn=2e5+5;
int n;
int a[maxn];
int main(){
int t;
for(cin>>t;t;t--){
cin>>n;
if(n<=32){//n<=32时,可以用2来除最后的n
int count=0,x=n;
while(x!=1){
count++;
x=((x+1)>>1);
}
cout<<count+n-3<<endl;
for(int i=3;i<=n-1;i++)cout<<i<<' '<<i+1<<endl;
while(count--)cout<<n<<' '<<2<<endl;
}
else{//否则用8来出最后的n
int count=0,x=n;
while(x!=1){
count++;
x=((x+7)/8);//向上取整
}
cout<<n-4+3+count<<endl;
for(int i=3;i<=n-1;i++){
if(i==8)continue;
cout<<i<<' '<<i+1<<endl;
}
while(count--)cout<<n<<' '<<8<<endl;
cout<<8<<' '<<2<<endl;
cout<<8<<' '<<2<<endl;
cout<<8<<' '<<2<<endl;
}
}
}
复杂度
O ( n ) O(n) O(n)
I. Pekora and Trampoline差分
题意
有n个跳床,每个跳床有一个跳的距离,每个跳床跳一次就会使得弹跳距离减1,弹跳距离max(1,a[i])。你可以选择任意跳床选择开始,问你最少经过几个轮回才能将所有跳床弹跳距离都变成1。
数据范围: 1 ≤ n ≤ 5000 , 1 ≤ s i ≤ 1 0 9 1 \leq n \leq 5000,1 \leq s_i\leq 10^9 1≤n≤5000,1≤si≤109
tags:差分,贪心
思路
**贪心:**从第一个弹力不为1的蹦床开始跳,直到它=1,在继续下一个蹦床
**差分:**要直到从一个蹦床开始弹跳会对让后面的蹦床有怎样的白嫖机会
- 若si+i>n,则一直蹦直到si+i=n
- 若si+i<=n,要进行总共s[i]-1轮从该蹦床出发使其变为1,那么他会让第i+2到第si+i个蹦床都白嫖一次
- 如果先前基类的白嫖次数count[i]>s[i]-1,那么它就可以不用进行s[i]-1轮了,并且会让第i+1个蹦床有count[i]-s[i]+1次白嫖
- 若count[i]<=s[i]-1那么就要进行s[i]-1-count[i]轮补充来实现总轮数为s[i]-1
代码
#include<iostream>
#define ll long long
using namespace std;
const int maxn=5e3+4;
int n;
ll s[maxn],count[maxn];
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
cin>>n;
for(int i=1;i<=n;i++){
cin>>s[i];
count[i]=0;
}
ll ans=0;
for(int i=1;i<=n;i++){
count[i]+=count[i-1];//count是差分数组,求前缀和来求积累的白嫖次数
int res=0;
if(s[i]+i>n){//若s[i]+i>n要进行s[i]+i-n轮使s[i]+i=n
res+=s[i]+i-n;
s[i]=n-i;
}
res+=s[i]-1;//在需进行s[i]-1轮使该蹦床弹力为0
if(count[i]>res)count[i+1]+=count[i]-res,count[i+2]-=count[i]-res;
count[i+2]++,count[i+s[i]+1]--;//利用差分数组来更新对后面蹦床的白嫖次数
ans+=max(0ll,res-count[i]);//看先前积累的白嫖数count[i]是否达到所需轮数,不够就得ans+=来补充了
}
cout<<ans<<endl;
}
}
复杂度
O ( n ) O(n) O(n)
J. RPD and Rap Sheet (Easy Version)交互好题
题意
tags:交互,思维,构造
思路
可以知道旧密码^询问值=新密码
我们从0到n-1遍历旧密码
对于询问值,我们需要猜,直到猜对返回1为止。假设遍历到第x个旧密码我们已经猜了的询问值为 a 1 , a 2 , a 3 . . . a x a_1,a_2,a_3...a_x a1,a2,a3...ax
不要关中间变化的新密码,只用管最后的新密码,并且假设旧密码值一直不会变
我们这次的新密码就是x^a[1]^a[2]^...^a[x]
,我们此次就猜这个数,如果不对说明我们就旧密码是错的,就换一个旧密码,并将此次猜的数字加入到询问值的异或和中
因为旧密码是0到n-1里的一个数,所以一定可以猜到新密码的
代码
#include<iostream>
#define ll long long
using namespace std;
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;
for(cin>>t;t;t--){
int n,k;
cin>>n>>k;
ll res=0,sum=0;
for(ll i=0;i<n;i++){//假设旧密码为i
ll x=(i^sum);//那么新密码就为i^sum,我们就猜i^sum
cout<<x<<endl;
int judge;
cin>>judge;//若judge为1,说明新密码就是我们猜的,也说明旧密码是i
if(judge)break;
sum^=x;//若judge为0,说明旧密码不是i,继续遍历旧密码,但是这次猜测我们还是得加入到询问异或和中
}
}
}
复杂度
O ( n ) O(n) O(n)