2021ICPC南京区域赛ACDJM
A. Oops, It’s Yesterday Twice More
题意
有一个 n × n ( 2 ≤ n ≤ 500 ) n\times n\ \ (2\leq n\leq 500) n×n (2≤n≤500)网格,每个网格上有一只袋鼠.求一组移动操作使得它们都移动到给定的点 ( a , b ) ( 1 ≤ a , b ≤ n ) (a,b)\ \ (1\leq a,b\leq n) (a,b) (1≤a,b≤n)处,要求移动次数不超过 3 ( n − 1 ) 3(n-1) 3(n−1).
操作用’U’、‘D’、‘L’、'R’分别表示全体袋鼠向上、向下、向左、向右移动一格,下一步移动会越界的袋鼠不动.
思路
显然只有所有袋鼠都移动到一个角落处才能统一行为.考察点 ( a , b ) (a,b) (a,b)离哪个角落更近(Manhattan距离,即移动的步数最少,若步数相等随便选一个),先将所有袋鼠移动到角落,再从该角落移动到点 ( a , b ) (a,b) (a,b).将所有袋鼠移动到一个角落所需的最小步数等于该角落对角的袋鼠移动到该角落的步数,即 ( n − 1 ) + ( n − 1 ) = 2 n − 2 (n-1)+(n-1)=2n-2 (n−1)+(n−1)=2n−2步.将所有袋鼠从一个角落移动到点 ( a , b ) (a,b) (a,b)所需步数为 ( a − 1 ) + ( b − 1 ) = a + b − 2 ≤ 2 n − 2 (a-1)+(b-1)=a+b-2\leq 2n-2 (a−1)+(b−1)=a+b−2≤2n−2.似乎加起来 ( 4 n − 4 ) (4n-4) (4n−4)步,超过了最大步数.但是 ( a , b ) = ( n , n ) (a,b)=(n,n) (a,b)=(n,n)时,点 ( a , b ) (a,b) (a,b)离得最近的角落即点 ( n , n ) (n,n) (n,n),则只需前 ( 2 n − 2 ) (2n-2) (2n−2)步即可完成.最坏的情况是 n n n为奇数时的网格的中心点 ( n + 1 2 , n + 1 2 ) \left(\dfrac{n+1}{2},\dfrac{n+1}{2}\right) (2n+1,2n+1),此时它离各个角落一样远,不妨设都移动到 ( 1 , 1 ) (1,1) (1,1),则总步数 ( 2 n − 2 ) + ( n + 1 2 − 1 ) + ( n + 1 2 − 1 ) = 3 ( n − 1 ) (2n-2)+\left(\dfrac{n+1}{2}-1\right)+\left(\dfrac{n+1}{2}-1\right)=3(n-1) (2n−2)+(2n+1−1)+(2n+1−1)=3(n−1),满足要求.
代码 -> 2021ICPC南京-A(思维)
int n; // 地图大小
int a, b; // 目标点
int main() {
cin >> n >> a >> b;
// 点(a,b)离左上、右上、左下、右下角的Manhattan距离
int lup = a - 1 + b - 1, rup = a - 1 + n - b, ldown = n - a + b - 1, rdown = n - a + n - b;
int mindis = min({ lup,rup,ldown,rdown });
if (lup == mindis) { // 离左上角最近
for (int i = 1; i < n; i++) cout << "UL"; // 移动到左上角
// 移动到(a,b)
for (int i = 1; i < a; i++) cout << 'D';
for (int i = 1; i < b; i++) cout << 'R';
}
else if (rup == mindis) { // 离右上角最近
for (int i = 1; i < n; i++) cout << "UR"; // 移动到右上角
// 移动到(a,b)
for (int i = 1; i < a; i++) cout << 'D';
for (int i = n; i > b; i--) cout << 'L';
}
else if (ldown == mindis) { // 离左下角最近
for (int i = 1; i < n; i++) cout << "DL"; // 移动到左下角
// 移动到(a,b)
for (int i = n; i > a; i--) cout << 'U';
for (int i = 1; i < b; i++) cout << 'R';
}
else { // 离右下角最近
for (int i = 1; i < n; i++) cout << "DR"; // 移动到右下角
// 移动到(a,b)
for (int i = n; i > a; i--) cout << 'U';
for (int i = n; i > b; i--) cout << 'L';
}
}
M. Windblume Festival ( 3 s 3\ \mathrm{s} 3 s)
题意
有 T T T组测试数据.每组测试数据有 n ( 1 ≤ n ≤ 1 e 6 ) n\ \ (1\leq n\leq 1\mathrm{e}6) n (1≤n≤1e6)个数 a i ( − 1 e 9 ≤ a i ≤ 1 e 9 , 1 ≤ i ≤ n ) a_i\ \ (-1\mathrm{e}9\leq a_i\leq 1\mathrm{e}9,1\leq i\leq n) ai (−1e9≤ai≤1e9,1≤i≤n)围成一圈,其中第 x ( 1 ≤ x ≤ n ) x\ \ (1\leq x\leq n) x (1≤x≤n)个数与第 ( x m o d n ) + 1 (x\ \mathrm{mod}\ n) +1 (x mod n)+1个数相邻.现进行若干次操作直至只剩下一个数:设当前还剩下 k ( 2 ≤ k ≤ n ) k\ \ (2\leq k\leq n) k (2≤k≤n)个数,每次操作取定一个 x ( 1 ≤ x ≤ k ) x\ \ (1\leq x\leq k) x (1≤x≤k),将令 a i ← a i − a ( i m o d k ) + 1 a_i\leftarrow a_i-a_{(i\ \mathrm{mod}\ k)+1} ai←ai−a(i mod k)+1后删去 a ( i m o d k ) + 1 a_{(i\ \mathrm{mod}\ k)+1} a(i mod k)+1,删去后 a i a_i ai与 a ( i m o d k ) + 1 a_{(i\ \mathrm{mod}\ k)+1} a(i mod k)+1的下一个相邻.求最后剩下的数的最大值.数据保证所有测试数据的 n n n之和不超过 1 e 6 1\mathrm{e}6 1e6.
思路
猜测最后剩下的数的最大值非负,若不全为零,则最大值为正.
下面以 n = 4 n=4 n=4为例推导贪心策略:
(i) 原数列中有正有负时,先考虑只有一个负数的情况.设当前环为 p 1 → n → p 2 → p 3 → p 1 ( n < 0 , p i > 0 ) p_1\rightarrow n\rightarrow p_2\rightarrow p_3\rightarrow p_1\ \ (n<0,p_i>0) p1→n→p2→p3→p1 (n<0,pi>0).不妨设 p 1 = max { p 1 , p 2 , p 3 } p_1=\max\{p_1,p_2,p_3\} p1=max{p1,p2,p3}.要使得最后剩下的数最大,最后一次操作应为 p 1 p_1 p1减去一个尽量大的负数,显然该负数是 n − p 2 − p 3 n-p_2-p_3 n−p2−p3,则最后剩下的数为 p 1 − ( n − p 2 − p 3 ) = p 1 + p 2 + p 3 − n = p 1 + p 2 + p 3 + ∣ n ∣ > 0 p_1-(n-p_2-p_3)=p_1+p_2+p_3-n=p_1+p_2+p_3+|n|>0 p1−(n−p2−p3)=p1+p2+p3−n=p1+p2+p3+∣n∣>0.显然这可推广到多个负数的情况.
(ii) 原数列全为正时,设当前环为 p 1 → p 4 → p 2 → p 3 → p 1 p_1\rightarrow p_4\rightarrow p_2\rightarrow p_3\rightarrow p_1 p1→p4→p2→p3→p1,且 p 1 > p 2 > p 3 > p 4 > 0 p_1>p_2>p_3>p_4>0 p1>p2>p3>p4>0.为使得最后剩下的数最大,应让原本最大的 p 1 p_1 p1减去一个尽量小的数,最好能减去负数.策略:① p 4 ← p 4 − p 2 p_4\leftarrow p_4-p_2 p4←p4−p2,此时 p 4 < 0 p_4<0 p4<0;② p 4 ← p 4 − p 3 p_4\leftarrow p_4-p_3 p4←p4−p3,此时 p 4 < 0 p_4<0 p4<0;③ p 1 ← p 1 − p 4 p_1\leftarrow p_1-p_4 p1←p1−p4,此时 p 1 > 0 p_1>0 p1>0且最大,最大值为 p 1 − p 4 = p 1 − ( p 4 − p 3 ) = p 1 − ( p 4 − p 2 − p 3 ) = p 1 + p 2 + p 3 − p 4 > 0 p_1-p_4=p_1-(p_4-p_3)=p_1-(p_4-p_2-p_3)=p_1+p_2+p_3-p_4>0 p1−p4=p1−(p4−p3)=p1−(p4−p2−p3)=p1+p2+p3−p4>0.
(iii) 原数列全为负时,设当前环为 n 1 → n 2 → n 3 → n 4 → n 1 n_1\rightarrow n_2\rightarrow n_3\rightarrow n_4\rightarrow n_1 n1→n2→n3→n4→n1,且 0 > n 1 > n 2 > n 3 > n 4 0>n_1>n_2>n_3>n_4 0>n1>n2>n3>n4.为使得最后剩下的数最大,应让原本最大的 n 1 n_1 n1减去一个尽量大的负数,或尝试让 n 1 n_1 n1变为正数,再减去一个尽量大的负数.策略:① n 1 ← n 1 − n 2 n_1\leftarrow n_1-n_2 n1←n1−n2,此时 n 1 > 0 n_1>0 n1>0;② n 1 ← n 1 − n 3 n_1\leftarrow n_1-n_3 n1←n1−n3,此时 n 1 > 0 n_1>0 n1>0;③ n 1 ← n 1 − n 4 n_1\leftarrow n_1-n_4 n1←n1−n4,此时 n 1 > 0 n_1>0 n1>0且最大,最大值为 n 1 − n 4 = ( n 1 − n 3 ) − n 4 = ( n 1 − n 2 − n 3 ) − n 4 = n 1 + ∣ n 2 ∣ + ∣ n 3 ∣ + ∣ n 4 ∣ > 0 n_1-n_4=(n_1-n_3)-n_4=(n_1-n_2-n_3)-n_4=n_1+|n_2|+|n_3|+|n_4|>0 n1−n4=(n1−n3)−n4=(n1−n2−n3)−n4=n1+∣n2∣+∣n3∣+∣n4∣>0.
(iv) 原数列中有零时,将零都视为正数或负数,划归到上述三种情况.
显然上述贪心策略可推广到有 n n n个数的情况,也证明了最后剩下的数的最大值非负,若不全为零,则最大值为正.
合并结论: a n s = { ∑ i = 1 n ∣ a i ∣ , 原数列有正有负时 ∑ i = 1 n ∣ a i ∣ − 2 ∣ a x ∣ , 其余情况 , 其中 ∣ a x ∣ 是绝对值最小者 ( n ≥ 2 ) ans=\begin{cases}\displaystyle\sum_{i=1}^n |a_i|,原数列有正有负时 \\ \displaystyle\sum_{i=1}^n |a_i|-2|a_x|,其余情况,其中|a_x|是绝对值最小者 \end{cases}\ \ (n\geq 2) ans=⎩ ⎨ ⎧i=1∑n∣ai∣,原数列有正有负时i=1∑n∣ai∣−2∣ax∣,其余情况,其中∣ax∣是绝对值最小者 (n≥2). n = 1 n=1 n=1时不符合该结论,要特判.
代码 -> 2021ICPC南京-M(贪心)
int n; // 数的个数
int main() {
CaseT{
cin >> n;
bool flag1 = 0, flag2 = 0; // 有正数、有负数
ll res = 0; // 绝对值之和
int minabs = INF; // 绝对值最小值
if (n == 1) { // 特判n=1的情况
int tmp; cin >> tmp;
cout << tmp << endl;
continue;
}
while (n--) {
int tmp; cin >> tmp;
if (tmp > 0) flag1 = 1;
else if (tmp < 0) flag2 = 1;
res += abs(tmp);
minabs = min(minabs, abs(tmp));
}
cout << (res - (ll)2 * (flag1 + flag2 != 2) * minabs) << endl;
}
}
C. Klee in Solitary Confinement
题意
给定一个长度为 n ( 1 ≤ n ≤ 1 e 6 ) n\ \ (1\leq n\leq 1\mathrm{e}6) n (1≤n≤1e6)的序列 a i ( − 1 e 6 ≤ a i ≤ 1 e 6 , 1 ≤ i ≤ n ) a_i\ \ (-1\mathrm{e}6\leq a_i\leq 1\mathrm{e}6,1\leq i\leq n) ai (−1e6≤ai≤1e6,1≤i≤n)和一个操作数 k ( − 1 e 6 ≤ k ≤ 1 e 6 ) k\ \ (-1\mathrm{e}6\leq k\leq 1\mathrm{e}6) k (−1e6≤k≤1e6).可至多进行一次操作:选定一个区间 [ l , r ] ( 1 ≤ l ≤ r ≤ n ) [l,r]\ \ (1\leq l\leq r\leq n) [l,r] (1≤l≤r≤n),将其中的数 + = k +=k +=k.求最后序列中的众数的出现次数.
思路I
一边读入一边用一个数组 c n t [ ] cnt[] cnt[]统计每个数出现的个数,并更新当前序列中的众数出现的次数 a n s ans ans.因序列的元素最小是 − 1 e 6 − 1 e 6 = − 2 e 6 -1\mathrm{e}6-1\mathrm{e}6=-2\mathrm{e}6 −1e6−1e6=−2e6,则序列加一个 2 e 6 2\mathrm{e}6 2e6的偏移量映射到 0 ∼ 4 e 6 0\sim 4\mathrm{e}6 0∼4e6,保证下标非负.
若 k = 0 k=0 k=0,则操作不改变 a n s ans ans,直接输出 a n s ans ans;若 k ≠ 0 k\neq 0 k=0,扫一遍序列,用一个数组 t i m e s [ ] times[] times[]统计进行操作后 a [ i ] a[i] a[i]和 a [ i ] + k a[i]+k a[i]+k的出现次数,即 t i m e s [ a [ i ] ] = max { 0 , t i m e s [ a [ i ] ] − 1 } , t i m e s [ a [ i ] + k ] + + times[a[i]]=\max\{0,times[a[i]]-1\},times[a[i]+k]++ times[a[i]]=max{0,times[a[i]]−1},times[a[i]+k]++,对 a [ i ] + k a[i]+k a[i]+k统计答案.
代码I -> 2021ICPC南京-C(思维)
const int MAXN = 4e6 + 5, offset = 2e6;
int n, k; // 数的个数、加数
int a[MAXN]; // 原数组+=2e6映射到0~4e6的数组
int cnt[MAXN]; // 每个数出现的次数
int ans;
int times[MAXN]; // 进行操作后每个数出现的次数
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++) {
cin >> a[i];
a[i] += offset; // 映射到0~4e6
cnt[a[i]]++;
ans = max(ans, cnt[a[i]]); // 当前众数出现的次数
}
if (!k) return cout << ans, 0; // k=0时答案即众数的出现次数
for (int i = 0; i < n; i++) { // 扫一遍序列,统计对每个a[i]+=k后的出现次数
times[a[i] + k]++; // 对a[i]+k贡献+1
times[a[i]] = max(0, times[a[i]] - 1); // 对a[i]的贡献-1,注意出现次数最少为0
ans = max(ans, times[a[i] + k] + cnt[a[i] + k]); // 对a[i]+k统计答案
}
cout << ans;
}
思路II
对 ∀ x ∈ [ 0 , 4 e 6 ] \forall x\in[0,4\mathrm{e}6] ∀x∈[0,4e6],只需考察 x x x和 x − k x-k x−k出现的次数.用一个vector数组 c n t [ ] cnt[] cnt[]来记录每个数出现的形式(以 x x x出现或以 x − k x-k x−k出现),读入时将 a [ i ] a[i] a[i]插入到 c n t [ a [ i ] ] cnt[a[i]] cnt[a[i]]和 c n t [ a [ i ] + k ] cnt[a[i]+k] cnt[a[i]+k]中.
对固定的 x x x,设 c n t [ x ] cnt[x] cnt[x]中有 m m m个数. s u m [ i ] [ 0 ] sum[i][0] sum[i][0]、 s u m [ i ] [ 1 ] sum[i][1] sum[i][1]分别表示 c n t [ x ] cnt[x] cnt[x]的前 i ( 1 ≤ i ≤ m ) i\ \ (1\leq i\leq m) i (1≤i≤m)个数中 x x x、 x − k x-k x−k出现的次数.对每个固定的 x x x,对区间 [ l , r ] [l,r] [l,r]进行操作后得到的答案 a n s j = s u m [ m ] [ 0 ] − ( s u m [ r ] [ 0 ] − s u m [ l − 1 ] [ 0 ] ) + ( s u m [ r ] [ 1 ] − s u m [ l − 1 ] [ 1 ] ) ans_j=sum[m][0]-(sum[r][0]-sum[l-1][0])+(sum[r][1]-sum[l-1][1]) ansj=sum[m][0]−(sum[r][0]−sum[l−1][0])+(sum[r][1]−sum[l−1][1])
= s u m [ m ] [ 0 ] + ( s u m [ r ] [ 1 ] − s u m [ r ] [ 0 ] ) + ( s u m [ l − 1 ] [ 0 ] − s u m [ l − 1 ] [ 1 ] ) =sum[m][0]+(sum[r][1]-sum[r][0])+(sum[l-1][0]-sum[l-1][1]) =sum[m][0]+(sum[r][1]−sum[r][0])+(sum[l−1][0]−sum[l−1][1]).要使得 a n s j ans_j ansj最大,只需 s u m [ r ] [ 1 ] − s u m [ r ] [ 0 ] sum[r][1]-sum[r][0] sum[r][1]−sum[r][0]和 s u m [ l − 1 ] [ 0 ] − s u m [ l − 1 ] [ 1 ] sum[l-1][0]-sum[l-1][1] sum[l−1][0]−sum[l−1][1]都取最大. x x x的答案 a n s x = max 1 ≤ j ≤ m a n s j \displaystyle ans_x=\max_{1\leq j\leq m}ans_j ansx=1≤j≤mmaxansj,最终答案 a n s = max 0 ≤ x ≤ 4 e 6 a n s x \displaystyle ans=\max_{0\leq x\leq 4\mathrm{e}6}ans_x ans=0≤x≤4e6maxansx.
外层循环枚举 0 ∼ 4 e 6 0\sim 4\mathrm{e}6 0∼4e6的 x x x,时间复杂度 O ( n ) O(n) O(n);内层循环枚举 c n t [ x ] cnt[x] cnt[x]的元素,因所有 x x x的 c n t [ x ] cnt[x] cnt[x]的元素个数之和为 2 n 2n 2n(分别为 x x x和 x − k x-k x−k),则每个 x x x的 c n t [ x ] cnt[x] cnt[x]平均有 2 n n = 2 \dfrac{2n}{n}=2 n2n=2个元素,故总平均复杂度 O ( 2 n ) O(2n) O(2n).
代码II -> 2021ICPC南京-C(思维+前缀和) (By: loop_up)
const int MAXN = 1e6 + 5, MAXM = MAXN << 2, offset = 2e6;
int n, k; // 数的个数、加数
vi cnt[MAXM]; // 每个数出现的次数
int ans;
int sum[MAXN][2]; // sum[i][0]、sum[i][1]分别表示cnt[x]的前i个数中x、x-k出现的次数
int main() {
cin >> n >> k;
for (int i = 0; i < n; i++) {
int tmp; cin >> tmp;
tmp += offset; // 映射到0~4e6
cnt[tmp].push_back(tmp), cnt[tmp + k].push_back(tmp);
ans = max({ ans,(int)cnt[tmp].size(),(int)cnt[tmp + k].size() }); // 更新当前可能的众数的出现次数
}
if (!k) return cout << (ans / 2), 0; // 特判k=0的情况,ans/2是因为x和x-k都被统计为x的出现次数
ans = 0; // 注意清空ans
for (int x = 0; x <= 4e6; x++) { // 枚举0~4e6的数
if (!cnt[x].size()) continue; // 没有该数
// 求每个cnt[x]中x和x-k出现次数的前缀和
for (int j = 0; j < cnt[x].size(); j++) {
sum[j + 1][0] = sum[j][0] + (cnt[x][j] == x); // 更新x出现的次数
sum[j + 1][1] = sum[j][1] + (cnt[x][j] != x); // 更新x-k出现的次数
}
ans = max(ans, sum[cnt[x].size()][0]); // 用x出现的次数更新当前众数的出现次数
int tmp = 0;
for (int j = 1; j <= cnt[x].size(); j++) {
tmp = max(tmp, sum[j - 1][0] - sum[j - 1][1]);
ans = max(ans, sum[cnt[x].size()][0] + sum[j][1] - sum[j][0] + tmp);
}
}
cout << ans;
}
思路III
在思路III的基础上,注意到 s u m [ j − 1 ] [ 0 ] − s u m [ j − 1 ] [ 1 ] = s u m [ j − 1 ] [ 0 ] ∗ 2 − ( s u m [ j − 1 ] [ 1 ] + s u m [ j − 1 ] [ 0 ] ) = s u m [ j − 1 ] [ 0 ] ∗ 2 − ( j − 1 ) sum[j-1][0]-sum[j-1][1]=sum[j-1][0]*2-(sum[j-1][1]+sum[j-1][0])=sum[j-1][0]*2-(j-1) sum[j−1][0]−sum[j−1][1]=sum[j−1][0]∗2−(sum[j−1][1]+sum[j−1][0])=sum[j−1][0]∗2−(j−1),可去掉 s u m [ ] [ ] sum[][] sum[][]的第二维,即只用 s u m [ i ] sum[i] sum[i]表示 c n t [ x ] cnt[x] cnt[x]的前 i i i个数中 x x x出现的次数.
代码III -> 2021ICPC南京-C(思维+前缀和+优化) (By: to cling)
const int MAXN = 1e6 + 5, MAXM = MAXN << 2, offset = 2e6;
int n, k; // 数的个数、加数
vi cnt[MAXM]; // 每个数出现的次数
int ans;
int sum[MAXN]; // sum[i]表示cnt[x]的前i个数中x出现的次数
int main() {
cin >> n >> k;
int maxnum = 0; // +=k后的数的最大值
for (int i = 0; i < n; i++) {
int tmp; cin >> tmp;
tmp += offset; // 映射到0~4e6
cnt[tmp].push_back(tmp), cnt[tmp + k].push_back(tmp);
maxnum = max(maxnum, tmp + k);
ans = max({ ans,(int)cnt[tmp].size(),(int)cnt[tmp + k].size() }); // 更新当前可能的众数的出现次数
}
if (!k) return cout << (ans / 2), 0; // 特判k=0的情况,ans/2是因为x和x-k都被统计为x的出现次数
ans = 0; // 注意清空ans
for (int x = 0; x <= maxnum; x++) { // 枚举0~maxnum的数
int m = cnt[x].size();
if (!m) continue; // 没有该数
for (int j = 1; j <= m; j++) // 求每个cnt[x]中x出现次数的前缀和
sum[j] = sum[j - 1] + (cnt[x][j - 1] == x);
int tmp = -1; // 注意是-1不是0
for (int j = 1; j <= m; j++) {
tmp = max(tmp, sum[j - 1] * 2 - j); // sum[j-1]*2-(j-1) = sum[j-1][0]-sum[j-1][1]
ans = max(ans, sum[m] + j + 1 - sum[j] * 2 + tmp);
}
}
cout << ans;
}
D. Paimon Sorting
题意
有 T T T组测试数据,每组测试数据给定一个长度为 n ( 1 ≤ n ≤ 1 e 5 ) n\ \ (1\leq n\leq 1\mathrm{e}5) n (1≤n≤1e5)的整数序列 a i ( 1 ≤ a i ≤ n ) a_i\ \ (1\leq a_i\leq n) ai (1≤ai≤n),用如下图所示的排序算法对该序列的每个非空前缀进行排序,分别输出排序过程中交换的次数.数据保证所有测试数据的 n n n之和不超过 1 e 6 1\mathrm{e}6 1e6.
思路
把该排序算法的过程打出来找规律,代码:
const int MAXN = 1e5 + 5;
int n; // 序列长度
int a[MAXN]; // 原数组
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (a[i] < a[j]) {
printf("swaped a[%d]=%d and a[%d]=%d\n", i, a[i], j, a[j]);
swap(a[i], a[j]);
ans++;
}
printf("[%d,%d] ", i, j);
for (int k = 1; k <= n; k++) cout << a[k] << ' ';
cout << endl << endl;
}
}
cout << ans;
}
测试数据I输入:
5
4 3 2 1 5
测试数据I输出:
[1,1] 4 3 2 1 5
[1,2] 4 3 2 1 5
[1,3] 4 3 2 1 5
[1,4] 4 3 2 1 5
swaped a[1]=4 and a[5]=5
[1,5] 5 3 2 1 4 // a[2]=3前有1个数比它大
// 第一趟排序把5换到了序列的开头
// 把4换到了序列的末尾
swaped a[2]=3 and a[1]=5
[2,1] 3 5 2 1 4
[2,2] 3 5 2 1 4
[2,3] 3 5 2 1 4
[2,4] 3 5 2 1 4
[2,5] 3 5 2 1 4 // a[3]=2前有2个数比它大
// 第二趟排序把5后移了一位
// 把3换到了序列的开头
swaped a[3]=2 and a[1]=3
[3,1] 2 5 3 1 4
swaped a[3]=3 and a[2]=5
[3,2] 2 3 5 1 4
[3,3] 2 3 5 1 4
[3,4] 2 3 5 1 4
[3,5] 2 3 5 1 4 // a[4]=1前有3个数比它大
// 第三趟排序把5后移了一位
// 把2换到序列的开头
swaped a[4]=1 and a[1]=2
[4,1] 1 3 5 2 4
swaped a[4]=2 and a[2]=3
[4,2] 1 2 5 3 4
swaped a[4]=3 and a[3]=5
[4,3] 1 2 3 5 4
[4,4] 1 2 3 5 4
[4,5] 1 2 3 5 4 // a[5]=4前有1个数比它大
// 第四趟排序把5后移了一位
// 把1换到序列的开头
[5,1] 1 2 3 5 4
[5,2] 1 2 3 5 4
[5,3] 1 2 3 5 4
swaped a[5]=4 and a[4]=5
[5,4] 1 2 3 4 5
[5,5] 1 2 3 4 5
// 第五趟排序把5后移了一位
8
可以发现,每趟排序后,序列的最大、次大、第三大等依次被换到序列开头,且排好的序列逐步后移.观察知,从第二趟排序开始,第 i ( 2 ≤ i ≤ n ) i\ \ (2\leq i\leq n) i (2≤i≤n)趟排序所需的交换次数等于第 ( i − 1 ) (i-1) (i−1)趟排序后的序列中 a [ i ] a[i] a[i]前比它大的数的个数,可用BIT来维护.
上述测试数据中无重复数字,下面讨论有重复数字的情况.
测试数据II输入:
5
2 3 2 1 5
测试数据II输出:
[1,1] 2 3 2 1 5
swaped a[1]=2 and a[2]=3
[1,2] 3 2 2 1 5
[1,3] 3 2 2 1 5
[1,4] 3 2 2 1 5
swaped a[1]=3 and a[5]=5
[1,5] 5 2 2 1 3 // a[2]=2前有1个数比它大
swaped a[2]=2 and a[1]=5
[2,1] 2 5 2 1 3
[2,2] 2 5 2 1 3
[2,3] 2 5 2 1 3
[2,4] 2 5 2 1 3
[2,5] 2 5 2 1 3
[3,1] 2 5 2 1 3 // a[3]=2前有一个数比它大
swaped a[3]=2 and a[2]=5
[3,2] 2 2 5 1 3
[3,3] 2 2 5 1 3
[3,4] 2 2 5 1 3
[3,5] 2 2 5 1 3 // a[4]=1前有3个数比它大,但其中有两个数相等,第4躺排序只交换了2次
swaped a[4]=1 and a[1]=2
[4,1] 1 2 5 2 3
[4,2] 1 2 5 2 3
swaped a[4]=2 and a[3]=5
[4,3] 1 2 2 5 3
[4,4] 1 2 2 5 3
[4,5] 1 2 2 5 3 // a[5]=3前有1个数比它大
[5,1] 1 2 2 5 3
[5,2] 1 2 2 5 3
[5,3] 1 2 2 5 3
swaped a[5]=3 and a[4]=5
[5,4] 1 2 2 3 5
[5,5] 1 2 2 3 5
7
观察知,上述结论中交换次数等于前面比它大的数的个数应去重.
i i i从 2 2 2开始从左往右扫一遍 a [ i ] a[i] a[i],遇到 a [ i ] > a [ 1 ] a[i]>a[1] a[i]>a[1]时交换并 a n s + + ans++ ans++,每一趟排序 a n s + = ans+= ans+=BIT的前缀和,即 a [ i ] a[i] a[i]前比其大的数的个数.
用这种思路模拟输入数据II:
原序列 2 3 2 1 5
a[2] = 3 > 2 = a[1] // ans++ swap(a[1], a[2]) = swap(2, 3)
新序列 3 2 2 1 5 // a[2]前有1个比它大的数,ans += 1 swap(a[2], a[1]) = swap(2, 3)
// a[3]前有1个比它大的数,ans += 1 swap(a[1], a[3]) = swap(2, 3)
a[5] = 5 > 3 = a[1] // ans++ swap(a[1], a[5]) = swap(3, 5)
新序列 5 2 2 1 3 // a[5]前有1个比它大的数,ans+=1 swap(a[1], a[5]) = swap(5, 3)
得到的ans = 5 < 7 // 少了swap(2, 1)和swap(2, 5)
从左往右扫一遍的过程中遇到 a [ i ] = a [ 1 ] a[i]=a[1] a[i]=a[1]时,将 f l a g flag flag置为 t r u e true true.当 f l a g = t r u e flag=true flag=true时,若 a [ i ] a[i] a[i]后 ∃ a [ j ] > a [ 1 ] \exists a[j]>a[1] ∃a[j]>a[1],则答案还需加上 a [ i ] a[i] a[i]和 a [ j ] a[j] a[j]间的数的个数 c n t cnt cnt.
代码 -> 2021ICPC南京-D(树状数组) (By: yl-9)
const int MAXN = 1e5 + 5;
int n; // 序列长度
int a[MAXN]; // 原数组
int BIT[MAXN]; // 树状数组
bool vis[MAXN]; // 记录某个数是否已加入集合,用于去重
int lowbit(int x) { return x & (-x); }
void add(int x) { for (int i = x; i <= n; i += lowbit(i)) BIT[i]++; } // 将数加入集合(单点修改)
int sum(int x) { // 求a[1...x]的前缀和
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += BIT[i];
return res;
}
int main() {
CaseT{
memset(BIT, 0, so(BIT));
memset(vis, false, so(vis));
cin >> n;
int maxnum = 0; // a[i]中的最大值
for (int i = 1; i <= n; i++) {
cin >> a[i];
maxnum = max(maxnum, a[i]);
}
ll ans = 0;
cout << ans; // 长度为1的前缀无需交换
bool flag = false; // 记录a[i]后是否出现与a[1]相等的元素
int cnt = 0; // 在flag=1时,若a[i]后存在一个>a[1]的a[j],则cnt统计a[i]与a[j]间的数的个数
vis[a[1]] = true, add(a[1]); // a[1]加入集合
for (int i = 2; i <= n; i++) {
if (!vis[a[i]]) vis[a[i]] = true, add(a[i]); // 去重后加入集合
if (a[i] == a[1]) flag = true; // 出现与a[1]相等的元素
cnt += flag - (flag ? a[i] > a[1] : 0); // flag=0时cnt不变,flag=1且a[i]<=a[1]时cnt++
if (a[i] > a[1]) { // 原a[i]==a[1]的位置后存在a[j]>a[1],需增加对答案的贡献
swap(a[1], a[i]);
ans += 1 + cnt; // 交换次数为中间的数的个数+1(交换a[1]和a[j])
cnt = flag = 0; // 清空
}
ans += sum(a[1]) - sum(a[i]); // a[i]前比其大的数的个数
cout << ' ' << ans;
}
cout << endl;
}
}
J. Xingqiu’s Joke ( 2 s 2\ \mathrm{s} 2 s)
题意
有 T ( 1 ≤ T ≤ 300 ) T\ \ (1\leq T\leq 300) T (1≤T≤300)组测试数据,每组测试数据输入两相异整数 a , b ( 1 ≤ a , b ≤ 1 e 9 ) a,b\ \ (1\leq a,b\leq 1\mathrm{e}9) a,b (1≤a,b≤1e9),每次进行如下三种操作之一,直至 a , b a,b a,b中至少有一个为 1 1 1,输出最少操作次数.操作:①两数同时 + 1 +1 +1;②两数同时 − 1 -1 −1;③两数同除以它们的一个公共素因子.
思路
在变化中寻找不变量,同时 + 1 +1 +1和 − 1 -1 −1让人联想到年龄的增长,注意到年龄差不变,可以发现加减过程中 c = a b s ( a − b ) c=abs(a-b) c=abs(a−b)不变,则每一组 ( a , b ) (a,b) (a,b)的公共素因子也是 c c c的素因子.可想到对 c c c素因数分解,枚举 c c c的所有素因子 d d d,进行状态 ( a , b , c ) (a,b,c) (a,b,c)到状态 ( a d , b d , c d ) \left(\dfrac{a}{d},\dfrac{b}{d},\dfrac{c}{d}\right) (da,db,dc)的转移.显然状态的前两维可只保留一个,不妨设 a < b a<b a<b,则只需保留 ( a , c ) (a,c) (a,c),因为最坏的情况要做 ( min { a , b } − 1 ) (\min\{a,b\}-1) (min{a,b}−1)次 − 1 -1 −1,从状态 ( a , c ) (a,c) (a,c)开始记搜.
考虑如何记录状态,最直接的想法是用一个map<pair<int,int>,int>来记数,担心超时可以用哈希表,用一个哈希函数来将每组 ( a , c ) (a,c) (a,c)映射为一个特征值,如 h a s h ( a , c ) = a ∗ 1 e 9 + c hash(a,c)=a*1\mathrm{e}9+c hash(a,c)=a∗1e9+c.
考察 d f s ( a , c ) dfs(a,c) dfs(a,c)的状态数.显然第二维的状态数即 c c c的因子个数,由熟知结论:int范围内因数最多的数有 1600 1600 1600个因子.考察第一维的状态数,即 a d \dfrac{a}{d} da上下取整共会产生多少个数.注意到int范围内的数至多有 30 30 30个素因子(即 30 30 30个 2 2 2),此时 a d \dfrac{a}{d} da上取整或下取整分别产生的数的个数约 log 2 a \log_2 a log2a个,约 30 30 30个,总共加起来约 60 60 60个.考察 a a a除以 d i d_i di的顺序不同会不会导致结果数不同,猜测不会,理由在补充部分说明,此处先用约 60 × 1600 × 300 = 2.88 e 7 60\times 1600\times 300=2.88\mathrm{e}7 60×1600×300=2.88e7的时间复杂度玄学过题.
代码 -> 2021ICPC南京-J(数论+记搜)
vi divisor; // 存c=abs(a-b)的素因子
map<pii, int> dp; // dp[{a,c}]表示dfs(a,c)的结果
void get(int c) { // 将c素因数分解
for (int i = 2; i * i <= c; i++) {
if (c % i == 0) {
divisor.push_back(i);
while (c % i == 0) c /= i;
}
}
if (c > 1) divisor.push_back(c);
}
int dfs(int a, int c) {
if (a == 1) return 0; // 已开锁
if (c == 1) return a - 1; // 无法再使用操作3,只能不断-1直至a=1
if (dp[make_pair(a, c)]) return dp[make_pair(a, c)]; // 搜过的
int res = a - 1; // 至多需要不断-1直至a=1,注意不能初始化为INF
for (auto d : divisor) { // 枚举初始的abs(a-b)的因子
if (c % d == 0) {
int rest = a % d; // a差几步可以变为d的倍数
int tmp1 = rest + 1 + dfs(a / d, c / d); // 做rest次-1,再做一次操作3,再加上a=floor(a/d)后的次数
int tmp2 = (d - rest) + 1 + dfs(a / d + 1, c / d); // 做(d-rest)次+1,再做一次操作3,再加上a=ceil(a.d)后的次数
res = min({ res,tmp1,tmp2 });
}
}
return dp[make_pair(a, c)] = res;
}
int main() {
CaseT{
divisor.clear();
dp.clear();
int a, b; cin >> a >> b;
int c = abs(a - b);
get(c); // 将c素因数分解
cout << dfs(min(a, b), c) << endl;
}
}
补充 (By: cjb)
对整数 x x x和素因子 d i d_i di,将 x x x以任意顺序整除 d i d_i di(无论上取整还是下取整),结果至多有两种,故总状态数依旧是 c c c的因数的级别.理由:设 x x x以任意顺序整除 d i d_i di的结果是 k 1 k_1 k1,全部进行上取整的结果是 k 2 k_2 k2.可用数归证明: k 1 ∏ i = 1 n d i ≤ x ≤ ( k 1 + 1 ) ∏ i = 1 n d i − 1 , ( k 2 − 1 ) ∏ i = 1 n d i + 1 ≤ x ≤ k 2 ∏ i = 1 n d i \displaystyle k_1\prod_{i=1}^n d_i\leq x\leq (k_1+1)\prod_{i=1}^n d_i -1,(k_2-1)\prod_{i=1}^n d_i+1\leq x\leq k_2\prod_{i=1}^n d_i k1i=1∏ndi≤x≤(k1+1)i=1∏ndi−1,(k2−1)i=1∏ndi+1≤x≤k2i=1∏ndi.将上述两式视为长度为 ( ∏ i = 1 n d i − 1 ) \displaystyle\left(\prod_{i=1}^n d_i-1\right) (i=1∏ndi−1)的两区间,则当且仅当 k 2 + 1 = k 1 k_2+1=k_1 k2+1=k1或 k 2 = k 1 k_2=k_1 k2=k1时两区间有交集.因全部进行上取整的结果最大,全部进行下取整的结果最小,则上下取整混合的结果在两者之间(类似于排序不等式:正序和 ≥ \geq ≥偏序和 ≥ \geq ≥反序和).