视频讲解:BV1CN411Z76w
A. Mean Inequality
题目大意
给定一个包含 2 n ( 1 ≤ n ≤ 25 ) 2n(1 \leq n \leq 25) 2n(1≤n≤25) 个不同的数的数组 a a a ,求数组 b b b 使其满足以下条件:
- b b b 是 a a a 的一种排列
- ∀ i ∈ [ 1 , 2 n ] \forall i \in [1,2n] ∀i∈[1,2n] , b i ≠ b i − 1 + b i + 1 2 b_i \neq \frac{b_{i-1}+b_{i+1}}{2} bi=2bi−1+bi+1 ,其中 b 0 = b 2 n b_0=b_{2n} b0=b2n , b 2 n + 1 = b 1 b_{2n+1}=b_{1} b2n+1=b1 。
题解
对于
b
b
b 中任意相邻的三个数,如果中间的数大于左右两个数,或小于左右两个数,那么必定可以满足条件。
对数组
a
a
a 排序,然后从小到大先填
b
b
b 中奇数位的数,再填偶数位的数。
那么得到的数组
b
b
b 中,奇数位的数必定小于相邻两个偶数位的数,偶数位的数必定大于相邻两个奇数位的数。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=110;
int a[MAXN];
int main()
{
int T,n,i;
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
n*=2;
for(i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
sort(a+1,a+n+1);
for(i=1;i<=n;i++)
{
if(i%2)
printf("%d ",a[i/2+1]);
else
printf("%d ",a[i/2+n/2]);
}
puts("");
}
}
B. I Hate 1111
题目大意
给定整数
x
(
1
≤
x
≤
1
0
9
)
x(1 \leq x \leq 10^9)
x(1≤x≤109) ,判断其等否表示为若干的
11
,
111
,
1111
,
11111
,
.
.
.
11,111,1111,11111,...
11,111,1111,11111,... 之和。
例如:
- 33 = 11 + 11 + 11 33=11+11+11 33=11+11+11
- 144 = 111 + 11 + 11 + 11 144=111+11+11+11 144=111+11+11+11
题解
会发现,
1111
=
11
∗
101
,
11111
=
11
+
111
∗
100
,
.
.
.
1111=11 * 101,11111=11+111 * 100, ...
1111=11∗101,11111=11+111∗100,... 即这些数都能表示为
11
11
11 和
111
111
111 的组合。因此若
x
x
x 可以表示为
11
a
+
111
b
(
0
≤
a
,
b
)
11a+111b(0 \leq a,b)
11a+111b(0≤a,b),则可以拆分,反之不行。
继续推导:
11
a
+
111
b
=
11
(
a
+
10
b
)
+
b
=
x
11a+111b=11(a+10b)+b=x
11a+111b=11(a+10b)+b=x
b ≡ x ( m o d 11 ) b \equiv x (mod \; 11) b≡x(mod11)
a + 10 b = x − b 11 a+10b = \frac{x-b}{11} a+10b=11x−b
a = x − b 11 − 10 b ≥ 0 a=\frac{x-b}{11}-10b \geq 0 a=11x−b−10b≥0
b b b 最小为 x % 11 x \% 11 x%11 ,因此 ⌊ x 11 ⌋ − 10 ( x % 11 ) ≥ 0 \lfloor \frac{x}{11} \rfloor -10(x\% 11) \geq 0 ⌊11x⌋−10(x%11)≥0 则有解,反之不行。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int main()
{
int T,x,y,n;
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
y=n%11;
if(n/11>=10*y)
printf("YES\n");
else
printf("NO\n");
}
}
C1+C2. Potions
题目大意
有
n
(
1
≤
n
≤
2
⋅
1
0
3
f
o
r
E
a
s
y
,
1
≤
n
≤
2
⋅
1
0
5
f
o
r
H
a
r
d
)
n(1 \leq n \leq 2 \cdot 10^3 for \; Easy,1 \leq n \leq 2 \cdot 10^5 for \; Hard)
n(1≤n≤2⋅103forEasy,1≤n≤2⋅105forHard) 个点排列在直线上,每个点上有一瓶药水,喝下第
i
i
i 个点上的药水会使得生命值增加
a
i
(
−
1
0
9
≤
a
i
≤
1
0
9
)
a_i(-10^9 \leq a_i \leq 10^9)
ai(−109≤ai≤109) ,如果为负值则表示会减少
∣
a
i
∣
|a_i|
∣ai∣ 。
初始生命中为
0
0
0 ,从左到右访问每个点,对于每瓶药水,可以选择喝它或无视它。求生命值始终为非负的情况下,最多能喝几瓶药水。
题解
动态规划解法(Easy)
设
d
p
i
,
j
dp_{i,j}
dpi,j 表示前
i
i
i 个点喝
j
j
j 瓶药水的情况下最后生命的最大值。
d
p
i
,
j
=
m
a
x
(
d
p
i
−
1
,
j
,
d
p
i
−
1
,
j
−
1
+
a
i
)
dp_{i,j}=max(dp_{i-1,j},dp_{i-1,j-1}+a_i)
dpi,j=max(dpi−1,j,dpi−1,j−1+ai)
如果遇到为负,则设为负无穷大表示不合法。
O
(
N
2
)
O(N^2)
O(N2) 复杂度内可以求出结果。
贪心解法1(Hard)
从左到右遍历每瓶药水:
- 若能喝则喝;
- 若不能喝,则考虑之前是否有喝过毒性更强药水,若有则用当前药水替代之。即放弃喝之前毒性更强的药水,选择喝当前药水;
维护历史喝过的药水并查询毒性最强的药水,即 a i a_i ai 最小值,可以用优先队列实现。
假设之前的最优策略为喝下 k k k 瓶药水,那么对于第 i i i 瓶药水:
- 若能喝第 i i i 瓶药水,则喝下后总数变为 k + 1 k+1 k+1,必然也是最优策略
- 若不能喝第 i i i 瓶药水,则考虑总数为 k k k 的情况下使得血量尽可能大,即选择之前所有药水中毒性最轻的 k k k 瓶药水喝。这个操作等价考虑用当前药水替代历史喝过的毒性最强的药水。
数学归纳法可以证明这个贪心策略成立。
参考代码(官方标程)
#include <bits/stdc++.h>
using namespace std;
int main(){
ios_base::sync_with_stdio(false); cin.tie(0);
int n; cin >> n;
priority_queue<long long, vector<long long>, greater<long long> > pq;
long long S = 0;
for(int i = 1;i <= n;i++){
long long x; cin >> x;
S += x;
pq.push(x);
while(S < 0){
S -= pq.top();
pq.pop();
}
}
cout << (int) pq.size();
}
贪心解法2(Easy+Hard)
首先所有 a i ≥ 0 a_i \geq 0 ai≥0 的药水都是能直接喝的,只需考虑 a i < 0 a_i <0 ai<0 的药水最多能喝多少。
设
h
p
i
hp_i
hpi 表示访问第
i
i
i 个点后的生命值,
f
(
x
)
f(x)
f(x) 表示访问第
i
i
i 个点以后的生命值最小值,即
f
(
x
)
=
m
i
n
i
=
x
n
h
p
i
f(x)=min_{i=x}^n{hp_i}
f(x)=mini=xnhpi 。
那么对于第
x
x
x 瓶药,若
f
(
x
)
≥
−
a
x
f(x) \geq -a_x
f(x)≥−ax 则能喝,反之会毒死不能喝。
设有 x , y x,y x,y 两瓶药水, 0 > a x > a y 0 > a_x > a_y 0>ax>ay ,即 第 i i i 瓶药水毒性更轻,会发现:
- 若 x > y x>y x>y ,明显第 x x x 瓶药水对 h p i hp_i hpi 值变化更小,即第 x x x 瓶药水更优;
- 若
x
<
y
x < y
x<y,考虑以下情况:两瓶药水最多只能喝一瓶,即
f ( x ) ≥ − a x , f ( y ) ≥ − a y , f ( y ) < − a x − a y f(x) \geq -a_x,f(y) \geq -a_y,f(y) < -a_x-a_y f(x)≥−ax,f(y)≥−ay,f(y)<−ax−ay
因此得到贪心策略:优先喝毒性小的药水,即 a i a_i ai 更大的药水。
对所有 a i < 0 a_i <0 ai<0 的药水排序,从大到小贪心考虑第 x x x 瓶药水能否喝,即 f ( x ) ≥ − a x f(x) \geq -a_x f(x)≥−ax 是否成立,若能则喝下, h p hp hp 值相应变化。若不能则忽略。
对于Easy版本,直接用数组模拟
h
p
hp
hp 变化即可。
对于Haed版本,用线段树优化。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=200100;
int num[MAXN];
struct Node
{
int l,r;
long long sum,inc;
}segtree[MAXN<<2];
void pushUp(int i)
{
segtree[i].sum=min(segtree[i<<1].sum,segtree[i<<1|1].sum);
}
void pushDown(int i)
{
if(segtree[i].inc)
{
segtree[i].sum+=segtree[i].inc;
segtree[i<<1].inc+=segtree[i].inc;
segtree[i<<1|1].inc+=segtree[i].inc;
segtree[i<<1].sum+=segtree[i].inc;
segtree[i<<1|1].sum+=segtree[i].inc;
segtree[i].inc=0;
}
}
void build(int i,int l,int r)
{
segtree[i].l=l;
segtree[i].r=r;
segtree[i].inc=0;
if(l==r)
{
segtree[i].sum=0;
return;
}
int mid=(l+r)>>1;
build(i<<1,l,mid);
build(i<<1|1,mid+1,r);
pushUp(i);
}
void add(int i,int a,int b,int c)
{
if(segtree[i].l==a && segtree[i].r==b)
{
segtree[i].inc+=c;
segtree[i].sum+=c;
return;
}
pushDown(i);
int mid=(segtree[i].l+segtree[i].r)>>1;
if(b<=mid)
add(i<<1,a,b,c);
else if(a>mid)
add(i<<1|1,a,b,c);
else
{
add(i<<1,a,mid,c);
add(i<<1|1,mid+1,b,c);
}
pushUp(i);
}
long long query(int i,int a,int b)
{
if(segtree[i].l==a && segtree[i].r==b)
{
return segtree[i].sum;
}
pushDown(i);
int mid=(segtree[i].l+segtree[i].r)>>1;
if(b<=mid)
return query(i<<1,a,b);
else if(a>mid)
return query(i<<1|1,a,b);
else
return min(query(i<<1,a,mid),query(i<<1|1,mid+1,b));
}
int cnt=0;
pair<int,int> p[MAXN];
int main()
{
int n,ans,i,x;
scanf("%d",&n);
build(1,1,n);
ans=0;
for(i=1;i<=n;i++)
{
scanf("%d",&x);
if(x>=0)
{
ans++;
add(1,i,n,x);
}
else
{
p[cnt++]=make_pair(x,i);
}
}
sort(p,p+cnt);
for(i=cnt-1;i>=0;i--)
{
add(1,p[i].second,n,p[i].first);
if(query(1,p[i].second,n)>=0)
ans++;
else
add(1,p[i].second,n,-p[i].first);
}
printf("%d\n",ans);
}
D. Kill Anton
题目大意
给定一个仅由 “A”,“N”,“O”,“T” 四种字母构成的字符串 a ( 1 ≤ ∣ a ∣ ≤ 1 0 5 ) a(1 \leq |a| \leq 10^5) a(1≤∣a∣≤105) ,求 a a a 的一种排列 b b b ,使得 b b b 为变化成 a a a 所需操作数最多的字符串。每次操作可以交换字符串中两个相邻字母。
题解
在最佳答案中,相同的字符将连续出现。
否则的话,例如
a
=
.
.
.
T
.
.
.
A
.
.
.
T
.
.
.
T
.
.
.
T
.
.
.
a=...T...A...T...T...T...
a=...T...A...T...T...T... ,对于其中的"A"和"T",
b
b
b 有两种方案:
- b = . . . A . . . T . . . T . . . T . . . T . . . b=...A...T...T...T...T... b=...A...T...T...T...T... ,"A"在T的左
- b = . . . T . . . T . . . T . . . T . . . A . . . b=...T...T...T...T...A... b=...T...T...T...T...A... ,"A"在T的右侧
- b = . . . T . . . T . . . A . . . T . . . T . . . b=...T...T...A...T...T... b=...T...T...A...T...T... ,"A"在T之间
明显,第二种方案最优,因为在
a
a
a 中"A"在"T"中偏左,应该使其在
b
b
b 中偏右会更好。
如上类推会发现,每种字母都应该在其他字母的某一侧会更优,即相同字母连续出现是最优情况。
因此可以枚举不同字符出现的顺序,找出操作数最大的方案。
由于相同字符连续出现,因此我们可以将
b
b
b 视为
a
a
a 按照某一种字母优先级规则排序后的结果。
因为将
b
b
b 变化为
a
a
a 的操作数,等于将
a
a
a 变化为
b
b
b 的操作数,因此求
b
b
b 到
a
a
a 的操作数就变成了对
a
a
a 进行排序所需的交换次数。而排序的交换次数就等于逆序数对。
因为只有四种字母,求逆序数对时可以直接枚举,省略树状数组的优化。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=100100;
const int MAXM=10;
int len,sum[MAXM],rk[MAXM],vis[MAXM],st[MAXM];
ll mx;
char s[MAXN],ans[MAXN],ch[MAXM];
map<char,int> mp;
void dfs(int dep,int id)
{
int i,j;
if(dep==4)
{
ll cur=0;
int sm[MAXM];
memset(sm,0,sizeof(sm));
for(i=0;i<len;i++)
{
for(j=0;j<4;j++)
{
if(rk[mp[s[i]]]<rk[j])
cur+=sm[j];
}
sm[mp[s[i]]]++;
}
if(cur>mx)
{
mx=cur;
for(i=0;i<4;i++)
{
for(j=st[i];j<st[i]+sum[i];j++)
ans[j]=ch[i];
}
}
}
for(i=0;i<4;i++)
{
if(vis[i])
continue;
vis[i]=1;
st[i]=id;
rk[i]=dep;
dfs(dep+1,id+sum[i]);
vis[i]=0;
}
}
int main()
{
int T,i;
mp['A']=0;mp['N']=1;mp['O']=2;mp['T']=3;
ch[0]='A';ch[1]='N';ch[2]='O';ch[3]='T';
scanf("%d",&T);
while(T--)
{
scanf("%s",&s);
len=strlen(s);
memset(sum,0,sizeof(sum));
for(i=0;i<len;i++)
{
sum[mp[s[i]]]++;
}
mx=-1;
dfs(0,0);
for(i=0;i<len;i++)
printf("%c",ans[i]);
puts("");
}
}
E. Oolimry and Suffix Array
题目大意
给定整数 n , k ( 1 ≤ n , k ≤ 2 ⋅ 1 0 5 ) n,k(1\leq n,k \leq 2 \cdot 10^5) n,k(1≤n,k≤2⋅105) 和长度为 n n n 后缀数组 s i ( 0 ≤ s i ≤ n − 1 ) s_i(0 \leq s_i \leq n-1) si(0≤si≤n−1),求长度为 n n n 的由 k k k 种字母构成的字符串中,后缀数组为 s i s_i si 的有多少种。
题解
考虑最少需要多少种字母,才能生成具有给定后缀数组的字符串。
考虑两个的后缀字符串 x y xy xy 和 a b ab ab ,满足 x y < a b xy < ab xy<ab,其中 x , a x,a x,a 为字符, y , b y,b y,b 为字符串。会得到以下结论:
- 若 y < b y < b y<b ,则必须 x ≤ a x \leq a x≤a ;
- 若 y > b y > b y>b ,则必须 x < a x < a x<a ;
因此当 x y < a b xy < ab xy<ab 且 y > b y > b y>b 时, x ≤ a − 1 x \leq a-1 x≤a−1 ,即需要引入一个新字母满足条件。
后缀数组有以下性质:
- 排序后相邻两个后缀字符串 s i s_i si 和 s i + 1 s_{i+1} si+1 的首字母必定满足 s i [ 0 ] ≤ s i + 1 [ 0 ] s_i[0] \leq s_{i+1}[0] si[0]≤si+1[0]
因此我们可以枚举排序后两个相邻的后缀字符串 x y , a b ( x y < a b ) xy,ab(xy < ab) xy,ab(xy<ab) ,判断 y y y 和 b b b 的大小,若 y > b y>b y>b ,则 x < a x < a x<a ,即表示最少所需字母集大小 + 1 +1 +1 ,反之 x ≤ a x \leq a x≤a。
具体而言,枚举后缀数组中两个相邻的数 a r r i , a r r i + 1 arr_i,arr_{i+1} arri,arri+1 ,判断 p o s [ a r r i + 1 ] > p o s [ a r r i + 1 + 1 ] pos[arr_i+1] > pos[arr_{i+1}+1] pos[arri+1]>pos[arri+1+1] 是否成立,其中 p o s [ x ] pos[x] pos[x] 表示 x x x 在后缀数组中的位置。若成立,则表示第 i i i 小的字符必须小于第 i + 1 i+1 i+1 小的字符。这样,就可以得到使得后缀数组成立所需的最小字母集大小 c n t + 1 cnt+1 cnt+1 (这里有 + 1 +1 +1 是因为还有一个最小的字母)。
在
n
n
n 个排序后的字符
c
i
c_i
ci 中,字母集大小为
k
k
k ,有
c
n
t
cnt
cnt 个字符比前一个字符大,剩余
n
−
c
n
t
n-cnt
n−cnt 个字符不小于前一个字符的方案数即是答案。
考虑在这些字符
c
i
c_i
ci 前添加
c
0
=
1
c_0=1
c0=1 ,在之后添加
c
n
+
1
=
k
c_{n+1}=k
cn+1=k ,然后对这些字符差分,那么差分后有
c
n
t
cnt
cnt 个元素必须
≥
1
\geq 1
≥1 ,其余
n
+
1
−
c
n
t
n+1-cnt
n+1−cnt 个元素必须
≥
0
\geq 0
≥0 。那么问题就可以转化为:
- 给定一个长度为 n + 1 n+1 n+1 的非负数组,求其总和为 k − c n t − 1 k-cnt-1 k−cnt−1 的方案数
这个问题可以转化:
- 将 k − c n t − 1 k-cnt-1 k−cnt−1 个球分为 n + 1 n+1 n+1 个不同的集合的方案数。
额外添加球使得集合要求变为非空:
- 将 n + k − c n t n+k-cnt n+k−cnt 个球分为 n + 1 n+1 n+1 个不同的非空集合的方案数。
用隔板法可等价于:
- 在
n
+
k
−
1
−
c
n
t
n+k-1-cnt
n+k−1−cnt 个间隔中选择
n
n
n 个间隔插上隔板的方案数,即
C n + k − 1 − c n t n C_{n+k-1-cnt}^{n} Cn+k−1−cntn
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=400400;
const ll mod=998244353;
ll fct[MAXN],invf[MAXN];
int arr[MAXN],pos[MAXN];
ll powmod(ll x,ll p)
{
ll ret=1;
while(p)
{
if(p&1)
ret=ret*x%mod;
x=x*x%mod;
p>>=1;
}
return ret;
}
ll C(ll n,ll m)
{
if(m>n||m<0)
return 0;
return fct[n]*invf[m]%mod*invf[n-m]%mod;
}
int main()
{
ll n,k,ans,cnt,i;
fct[0]=1;
for(i=1;i<MAXN;i++)
fct[i]=fct[i-1]*i%mod;
invf[MAXN-1]=powmod(fct[MAXN-1],mod-2);
for(i=MAXN-2;i>=0;i--)
invf[i]=invf[i+1]*(i+1)%mod;
scanf("%lld%lld",&n,&k);
for(i=0;i<n;i++)
{
scanf("%d",&arr[i]);
pos[arr[i]]=i;
}
pos[n]=-1;
cnt=0;
for(i=0;i<n-1;i++)
{
if(pos[arr[i]+1]>pos[arr[i+1]+1])
cnt++;
}
ans=C(n+k-1-cnt,n);
printf("%lld\n",ans);
}
F. Median Queries
题目大意
有一个隐藏的长度为 n ( 20 ≤ n ≤ 1 0 5 ) n(20 \leq n \leq 10^5) n(20≤n≤105) 的 1 1 1 到 n n n 的排序 p p p ,满足 p 1 < p 2 p_1 < p_2 p1<p2 。现在你需要通过交互问答猜出这个序列。
每次询问可以给出三个不同的整数 a , b , c ( 1 ≤ a , b , c ≤ n ) a,b,c(1 \leq a,b,c \leq n) a,b,c(1≤a,b,c≤n) ,会返回 { ∣ p a − p b ∣ , ∣ p a − p c ∣ , ∣ p b − p c ∣ } \{ |p_a-p_b|,|p_a-p_c|,|p_b-p_c|\} {∣pa−pb∣,∣pa−pc∣,∣pb−pc∣} 其中的中位数。
你需要在 2 n + 420 2n+420 2n+420 次询问内猜出排序 p p p 。
题解
尝试倒推问题。
设
q
u
e
r
y
(
a
,
b
,
c
)
query(a,b,c)
query(a,b,c) 为询问
x
,
y
,
z
x,y,z
x,y,z 得到的结果。
如果我们知道了
1
1
1 和
2
2
2 所在的位置
x
,
y
x,y
x,y ,即
p
x
=
1
,
p
y
=
2
p_x=1,p_y=2
px=1,py=2 ,那么对于其他数,询问
(
x
,
y
,
i
)
(x,y,i)
(x,y,i) ,即可得到
p
i
=
q
u
e
r
y
(
x
,
y
,
i
)
+
2
p_i=query(x,y,i)+2
pi=query(x,y,i)+2 。可以用
n
−
2
n-2
n−2 次询问得到排序结果。
如果知道
n
n
n 和
n
−
1
n-1
n−1 的位置,同样也可以得到结果。
考虑如何找到
1
1
1 和
2
2
2 的位置或
n
n
n 和
n
−
1
n-1
n−1 的位置。
假设我们知道了两个数
a
,
b
a,b
a,b 的位置
p
o
s
[
a
]
pos[a]
pos[a] 和
p
o
s
[
b
]
pos[b]
pos[b] ,并且这两个数之差
∣
a
−
b
∣
|a-b|
∣a−b∣ 比较小。那么对于其他数,我们可以询问
(
p
o
s
[
a
]
,
p
o
s
[
b
]
,
i
)
(pos[a],pos[b],i)
(pos[a],pos[b],i) ,得到返回结果最大的
i
i
i ,必定是
1
1
1 或
n
n
n 的位置。第二大结果的
j
j
j ,必定是
2
2
2 或
n
−
1
n-1
n−1 的位置。这样我们可以用
n
−
2
n-2
n−2 次询问找到
1
1
1 和
2
2
2 或
n
n
n 和
n
−
1
n-1
n−1 。
那么
∣
a
−
b
∣
|a-b|
∣a−b∣ 需要多小才能满足条件呢?会发现应满足
∣
a
−
b
∣
≤
⌊
n
−
4
3
⌋
|a-b| \leq \lfloor \frac{n-4}{3} \rfloor
∣a−b∣≤⌊3n−4⌋
才能保证
q
u
e
r
y
(
p
o
s
[
1
]
,
p
o
s
[
a
]
,
p
o
s
[
b
]
)
>
q
u
e
r
y
(
p
o
s
[
2
]
,
p
o
s
[
a
]
,
p
o
s
[
b
]
)
>
∣
a
−
b
∣
query(pos[1],pos[a],pos[b])>query(pos[2],pos[a],pos[b])>|a-b|
query(pos[1],pos[a],pos[b])>query(pos[2],pos[a],pos[b])>∣a−b∣
或
q
u
e
r
y
(
p
o
s
[
n
]
,
p
o
s
[
a
]
,
p
o
s
[
b
]
)
>
q
u
e
r
y
(
p
o
s
[
n
−
1
]
,
p
o
s
[
a
]
,
p
o
s
[
b
]
)
>
∣
a
−
b
∣
query(pos[n],pos[a],pos[b])>query(pos[n-1],pos[a],pos[b])>|a-b|
query(pos[n],pos[a],pos[b])>query(pos[n−1],pos[a],pos[b])>∣a−b∣
为了找到这样
a
,
b
a,b
a,b 的位置,不妨寻找三元组
a
,
b
,
c
a,b,c
a,b,c ,如果满足
q
u
e
r
y
(
p
o
s
[
a
]
,
p
o
s
[
b
]
,
p
o
s
[
c
]
)
≤
⌊
n
−
4
6
⌋
query(pos[a],pos[b],pos[c]) \leq \lfloor \frac{n-4}{6} \rfloor
query(pos[a],pos[b],pos[c])≤⌊6n−4⌋
那么必定可以得到
∣
a
−
b
∣
≤
2
⋅
⌊
n
−
4
6
⌋
≤
⌊
n
−
4
3
⌋
|a-b| \leq 2 \cdot \lfloor \frac{n-4}{6} \rfloor \leq \lfloor \frac{n-4}{3} \rfloor
∣a−b∣≤2⋅⌊6n−4⌋≤⌊3n−4⌋
注意,在上述过程中,我们并不需要知道 a , b , c a,b,c a,b,c 的具体值,只需要知道他们的位置 p o s [ a ] , p o s [ b ] , p o s [ c ] pos[a],pos[b],pos[c] pos[a],pos[b],pos[c] 。
寻找
a
,
b
,
c
a,b,c
a,b,c 的方法,可以通过随机选取,也可以在至少
13
13
13 个数内枚举所有三元组。
关于为什么
13
13
13 个数内至少有一个三元组的询问结果不超过
⌊
n
−
4
6
⌋
\lfloor \frac{n-4}{6} \rfloor
⌊6n−4⌋ 的证明如下:
- 设 d 1 , d 2 , . . . d 1 2 d_1,d_2,...d_12 d1,d2,...d12 表示相邻两个数的距离,如果任意两个连续距离的都不超过 ⌊ n − 4 6 ⌋ \lfloor \frac{n-4}{6} \rfloor ⌊6n−4⌋ ,那么就找到了结果。
- 由于 ∑ i = 1 12 d i = n − 1 \sum_{i=1}^{12}{d_i}=n-1 ∑i=112di=n−1 ,因此大于最多有 5 5 5 个 d i d_i di 不小于 ⌊ n − 4 6 ⌋ + 1 \lfloor \frac{n-4}{6} \rfloor+1 ⌊6n−4⌋+1 。
- 剩余 7 7 7 个最大为 ⌊ n − 4 6 ⌋ \lfloor \frac{n-4}{6} \rfloor ⌊6n−4⌋ 的 d i d_i di 中,必定至少有 2 2 2 个是相邻的。即必定有一个三元组的询问结果不超过 ⌊ n − 4 6 ⌋ \lfloor \frac{n-4}{6} \rfloor ⌊6n−4⌋ 。
参考代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=100100;
int p[MAXN];
vector<int> vec[MAXN];
int query(int a,int b,int c)
{
int ret;
cout<<"? "<<a<<" "<<b<<" "<<c<<endl;
cin>>ret;
return ret;
}
int main()
{
ios_base::sync_with_stdio(false), cin.tie(nullptr);
int T,n,i,j,k,a,b,mx,id1,id2,ret;
cin>>T;
while(T--)
{
cin>>n;
a=0;
for(i=1;i<=13&!a;i++)
for(j=i+1;j<=13&!a;j++)
for(k=j+1;k<=13&!a;k++)
{
if(query(i,j,k)<=(n-4)/6)
{
a=i,b=j;
break;
}
}
mx=-1;
for(i=1;i<=n;i++)
{
if(i==a||i==b)
continue;
ret=query(i,a,b);
vec[ret].push_back(i);
mx=max(ret,mx);
}
id1=vec[mx][0];
id2=vec[mx-1][0];
if(vec[mx-1].size()>=2)
{
if(query(id1,vec[mx-1][0],a)>query(id1,vec[mx-1][1],a))
id2=vec[mx-1][1];
}
p[id1]=1;
p[id2]=2;
for(i=1;i<=n;i++)
{
if(i==id1||i==id2)
continue;
p[i]=query(id1,id2,i)+2;
}
if(p[1]>p[2])
{
for(i=1;i<=n;i++)
p[i]=n-p[i]+1;
}
cout<<"!";
for(i=1;i<=n;i++)
cout<<" "<<p[i];
cout<<endl;
cin>>n;
for(i=0;i<=mx;i++)
vec[i].clear();
}
}