CF 1800~2100 思维题泛做
1648B
\(\color{Blue}{1800}\)
关键词:数学、算法优化。
给定长为 \(n\) 满足 \(\max\limits_{1 \le i\le n}\ {a_i} \le c\) 的序列 \(a\)。
求问是否能够保证序列 \(a\) 中所有满足 \(x\le y\) 的两数,\(\left\lfloor\dfrac{x}{y}\right\rfloor\) 也在序列中。
\(\sum \ n,\sum \ c \le 10^6,n,c\le 10^6.\)
- \(O(\sum n^2)\) 的算法
看到题目就能一眼出的算法,无非暴力枚举所有 \(x,y\) 判断。
因为有 \(c\le 10^6\),可以开一个桶存一个数是否出现过,这样就能做到 \(O(1)\) 判断。
时间复杂度 \(O(n^2)\)。
- \(O(\sum n \sqrt{n})\) 的算法
把直接枚举序列中的每个数,改成对于数 \(x\),枚举 \(\left[ 2,\sqrt{n} \right]\) 区间内的数。
时间复杂度降至 \(O(n \sqrt{n})\),但还需优化。
#define L(i,j,k) for(int i=(j);i<=(k);i++)
void work(){
cin>>n>>k;L(i,1,k) t[i]=0;
L(i,1,n) cin>>a[i],t[a[i]]=1;
if(!t[1]) return cout<<"No"<<endl,void();
L(i,1,n) L(j,2,(sqrt(a[i])))
if(!(t[j]+t[a[i]/j]^1))
return cout<<"No"<<endl,void();
cout<<"Yes"<<endl;
}
- \(O(\sum c \ln \, c)\) 的算法
正片开始!。
如果枚举因数不行,那就枚举倍数。
先对序列去重,特判序列中不存在 \(1\) 的情况直接判负。
对于 \(x \in \left[ i\times j,i \times \left( j+1 \right) \right)\),也就是 \(\left\lfloor \dfrac{x}{i} \right\rfloor = j\) 的情况,
如果 \(x,i \in a,j \not\in a\),则此序列不满足条件。
可以直接在 \(\left[1,c \right]\) 的范围内枚举 \(i\) 和相应的 \(j\) 并判断。
上面的式子可以想到用之前处理出桶数组进行前缀和 \(O(1)\) 判断。
时间复杂度 \(O(\sum c \ln \, c)\),详见调和级数——百度百科。
提交记录
1583D
\(\color{Blue}{1800}\)
关键词:交互、拆分任务。
存在一个长度为 \(n\) 的排列 \(a\),最多询问 \(2 \times n\) 次,求出排列 \(a\)。
每次询问提交一个序列 \(b\),会回复序列 \(a,b\) 之和所得序列中第一个出现不止一次的数的位置,没有则返回 \(0\)。
\(n\le 100\)。
从询问 \(2 \times n\) 的条件上看,可以想到将其拆成 \(n+n\) 次询问,分别实现不同的目标。
在这道题里,是从局部推至整体。
- 局部:
很明显,我们可以在 \(n\) 次询问内求得任意一个排列 \(a\) 中的数值。
除了要求的位置,序列 \(b\) 全赋值为 \(1\),所求位的值从 \(n\) 开始递减,假设值为 \(k\) 时返回值不为 \(0\),那么排列 \(a\) 当前位的值就是 \(n-k+1\)。
根据题目中第一个出现不止一次的数的位置
的描述,先求 \(a_n\) 可以避免位置的干扰。
- 整体:
只要我们知道了 \(a_n\),那就可以用 \(n-1\) 次询问求得排列 \(a\) 其余所有元素。
因为排列中每个数唯一,考虑将 \(a_n\) 补至 \(n+1\),就能保证询问提交序列的 \(\max\limits_{1\le i\le n} b_i \le n\)。
要求得数的位置必然在 \(a_n\) 之前,所以正确性也能保证。
#include <bits/stdc++.h>
using namespace std;
#define L(i,j,k) for(int i=(j);i<=(k);i++)
#define R(i,j,k) for(int i=(j);i>=(k);i--)
int a[200],ans[200],k=1,n,x;
void ask(){
cout<<"? ";L(j,1,n)
cout<<a[j]<<" \n"[j==n];
cout.flush();cin>>x;
}signed main(){
// freopen("test.in","r",stdin);
cin>>n;L(i,1,n-1) a[i]=1;
R(i,n,1){a[n]=i;ask();if(x){k=i;break;} }
ans[n]=n-k+1;a[n]=n+1-ans[n];
L(i,1,n){
L(j,1,n-1) a[j]=n+1-i;
ask();ans[x]=i;
}cout<<"! ";L(i,1,n)
cout<<ans[i]<<" \n"[i==n];
cout.flush();
}
询问次数 \(O(n)\) 的做法:
设询问的数组为 \(A\),最终答案数组 \(B\)。
跟之前的做法类似,设 \(A=[1,1, \dots , 1,k]\),但是 \(k\) 从 \(2,3,\dots\) 开始枚举。
当第一次的返回为 \(0\) 时,就可以确定 \(B_n=n-k+1\)
这一过程中返回的非 \(0\) 信息其实也能利用。
如果当 \(k=x\) 时询问的回复是 \(u\),那么很容易推得 \(B_u = B_n + x-1\)
这样就能在 \(n-B_n\) 次询问后,确定值在 \(\left[ B_n,n \right]\) 范围内的数的位置。
在剩下的 \(b_n-1\) 次询问中,先将 \(A_n\) 设为 \(1\),其余 \(B\) 中确定的值在 \(A\) 中对应的位置都设为 \(n\)。
总共循环 \(b_n-1\) 次,在第 \(i\) 次时,将 \(A\) 中所有不确定的位置全设为 \(i+1\),询问返回 \(u\),则可以确定 \(B_u=B_n-i\)。
这样,我们实现了在询问次数为 \(O(n)\) 的情况下解决了这个问题。
提交记录
1666C
\(\color{Blue}{1800}\)
关键词:构造。
给定二维平面上 \(3\) 个点,用总长度最短的线段连接这 \(3\) 个点。求线段数和每条线段的端点。
\(-10^9 \le x_i,y_i \le 10^9.\)
构造题,可以想到\(\text{最短的线段长度} \ge \max\limits_{1\le i \le 3} x_i -\min\limits_{1\le i \le 3} x_i +\max\limits_{1\le i \le 3} y_i - \min\limits_{1\le i \le 3} y_i\)。
将点按照 \(x\) 轴坐标排序,两边的点向中间连边,中间点过平行于 \(y\) 轴,包括三个点的线段。
如下图所示:
-----|
|
|-----
|
线段数恒为 \(3\)。
#include <bits/stdc++.h>
#define F(i) a[i].first
#define S(i) a[i].second
using namespace std;
pair <int,int> a[5];
int main(){
freopen("test.in","r",stdin);
for(int i=1;i<=3;i++) cin>>F(i)>>S(i);sort(a+1,a+4);
cout<<3<<'\n'<<F(1)<<' '<<S(1)<<' '<<F(2)<<' '<<S(1)<<'\n';
cout<<F(2)<<' '<<min(S(1),min(S(2),S(3)))<<' '<<F(2)<<' '<<max(S(1),max(S(2),S(3)))<<'\n';
cout<<F(2)<<' '<<S(3)<<' '<<F(3)<<' '<<S(3);return 0;
}
1583D
\(\color{Purple}{1900}\)
关键词:交互、转化信息、读题。
因为连着读错两次题导致几乎荒废了一整个下午。
其实还是比较容易知道,只要在两次询问中排除三个人就能保证在规定次数内求解。
其实题目已经限定的很死了,整个框架已经搭好。
如果只是两个人,比一下就行。
如果是四个人,就要考虑如何两次询问排除。
比如 \(1,2,3,4\) 四人。
头脑风暴可知,先比较 \(1,3\),根据结果分类讨论。
- 回复 \(0\)
此时 \(1,3\) 都是 \(0\) 场,比较 \(2,4\) 即可。
- 回复 \(1\)
此时 \(1\) 必定赢 \(2\),\(3\) 必定输 \(4\),比较 \(1,4\)。
- 回复 \(2\)
和上面差不多推理,比较 \(2,3\)。
如此实现了两次询问排除三人。
重复上述过程即可。
如果最后剩下两人就再比一次。
为什么读错题还是两次一起啊喂
int n;int ask(int x,int y){
cout<<"? "<<x<<' '<<y<<'\n';
cl;int g;cin>>g;return g;
}void out(int x){cout<<"! "<<x<<'\n';cl;}
void work(){
cin>>n;vi a(1<<n);iota(all(a),1);
while(a.size()>=4){
vi b;for(int i=0;i+3<a.size();i+=4){
int x=ask(a[i],a[i+2]);
if(!x) b.pb(ask(a[i+1],a[i+3])==1?a[i+1]:a[i+3]);
else if(x==1) b.pb(ask(a[i],a[i+3])==1?a[i]:a[i+3]);
else b.pb(ask(a[i+1],a[i+2])==1?a[i+1]:a[i+2]);
}a=b;
}out(a[0]=(a.size()==2)?a[ask(a[0],a[1])-1]:a[0]);
}
1701D
\(\color{Purple}{1900}\)
关键词:贪心,优先队列。
首先,根据题目给出的式子我们可以反向算出 \(a_i\) 的取值范围:\(\left[ \left\lfloor \dfrac{i}{b_i+1} \right\rfloor + 1, \left\lfloor \dfrac{i}{b_i} \right\rfloor \right]\)。
现在题目转化成了,每个点对应有若干个线段。
采取贪心策略。
对于一个点,将所有以这个点为左端点的线段存入对应的数组。
之后进行贪心,对于每个点,将其对应的线段压入优先队列,每次选取右端点最小的线段。
因为选取右端点最小,使得之后的选择余地更大。
最后注意下 \(b_i=0\) 的情况,不能直接除,直接赋值为 \(n\) 即可。
#define pb push_back
#define vi vector<int>
#define pi pair<int,int>
int n;priority_queue<pi>q;
const int N=5e5+100;
int a[N],b[N];
vi p[N];pi t[N];
void work(){
cin>>n;L(i,1,n) p[i].clear();
while(!q.empty()) q.pop();
L(i,1,n) cin>>a[i];
L(i,1,n){
t[i]={i/(a[i]+1)+1,a[i]?i/a[i]:n};
p[F(t[i])].pb(i);
}L(i,1,n){
for(int x:p[i]) q.push({-S(t[x]),x});
int x=S(q.top());q.pop();a[x]=i;
}L(i,1,n) cout<<a[i]<<" \n"[i==n];
}
1485D
\(\color{Gold}{2200}\)
关键词:人类智慧
题目中有个重要信息:\(1 \le a_{i,j} \le 16\),限制的如此之死,绝非偶然之举。
因为 \(\operatorname{lcm}(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16)=720720\),在题目给出的 \(10^6\) 范围内,所以可以进行构造。
进行黑白染色,区分是加上 \(a_{i,j}^4\) 还是不变即可。
时间复杂度:\(O(nm)\)
空间复杂度:\(O(1)\)
#define L(i,k) for(int i=1;i<=(k);i++)
void work(){
int n,m,x;cin>>n>>m;L(i,n) L(j,m)
cin>>x,cout<<720720+(((i+j)&1)?0:pow(x,4))<<" \n"[j==m];
}
纯粹就是思路特阴间,代码特好写的诈骗构造题。
所以才记录这样一道 2200 的题目