D. Blackslex and Penguin Civilization
时间限制:每测试点 2 秒
内存限制:每测试点 256 兆字节
问题描述
企鹅是文明的生物,它们使用排列进行交流。作为企鹅研究者,Blackslex 必须研究它们的交流方式。
对于一个给定的整数 n,考虑数组 [0, 1, \dots, 2^n - 1] 的一个排列 p。定义 $$ S(p) = \sum_{i=0}^{2^n-1} \text{popcount}(p_0 \& p_1 \& \dots \& p_i), $$ 其中 \text{popcount}(z) 表示 z 的二进制表示中 1 的个数(例如,\text{popcount}(5) = 2,因为 5 = 101_2 的二进制表示中有两个 1),而 \& 表示按位与运算。
如果一个排列能使 S(p) 最大化,则它被认为是神圣的。请找出字典序最小的神圣排列。
*长度 n 的排列是一个由 1 到 n 这 n 个不同的整数按任意顺序组成的数组。例如,[2,3,1,5,4] 是一个排列,但 [1,2,2] 不是排列(2 在数组中出现了两次),[1,3,4] 也不是排列(n=3 但数组中有 4)。
†如果两个相同大小的数组 a 和 b 在第一个不同的位置上,a 的元素小于 b 的对应元素,则称 a 字典序小于 b。
输入格式
第一行包含一个整数 t (1 \le t \le 16) —— 测试用例的数量。
每个测试用例包含一个整数 n (1 \le n \le 16)。
保证所有测试用例的 2^n 之和不超过 2^{16}。
输出格式
对于每个测试用例,输出 2^n 个整数 p_0, p_1, \dots, p_{2^n-1} —— 要求的排列。
样例输入
2
1
2
样例输出
1 0
3 1 0 2
样例说明
对于第一个测试用例,有两种可能的排列: - p = [0, 1],S(p) = 0 - p = [1, 0],S(p) = 1
对于第二个测试用例,S([3,1,0,2]) = 3 是神圣的。存在其他神圣的排列 p,例如 p = [3,2,0,1],但它们不是字典序最小的。
错误第一版
// Problem: CF 2179 D
// Contest: Codeforces - Codeforces Round 1071 (Div. 3)
// URL: https://codeforces.com/contest/2179/problem/D
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define ios ios::sync_with_stdio(false);cin.tie(nullptr);
void solve(){
int n;
cin>>n;
vector<int> a[20];
for(int i=0;i<1<<n;i++){
int v=__builtin_popcount(i);
a[v].push_back(i);
}
for(int i=0;i<=n;i++){
sort(a[i].begin(),a[i].end());
}
for(int i=n;i>=0;i--){
for(auto v:a[i]){
cout<<v<<" ";
}
}
cout<<endl;
return ;
}
int main(){
ios;
int t;
cin>>t;
while(t--){
solve();
}
return 0;
}
我的解题思路’
// Problem: CF 2179 D
// Contest: Codeforces - Codeforces Round 1071 (Div. 3)
// URL: https://codeforces.com/contest/2179/problem/D
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include<bits/stdc++.h>
using namespace std;
#define ios ios::sync_with_stdio(false);cin.tie(nullptr);
void solve(){
int n;
cin>>n;
vector<int> a((1<<n)+3),st((1<<n)+3,0);
int m=(1<<n)-1;
cout<<m<<" ";
st[m]=1;
for(int i=n-1;i>=0;i--){
m^=(1<<i);
st[m]=1;
cout<<m<<" ";
}
for(int i=1;i<1<<n;i++){
if(!st[i]) cout<<i<<" ";
}
cout<<endl;
return ;
}
int main(){
ios;
int t;
cin>>t;
while(t--){
solve();
}
return 0;
}
确实是好题目
你的第二版解题思路(贪心法)不是最优的,虽然它在 n=2 时能过样例,但在 n 更大时无法得到最大分数。
为什么你的思路有问题?
你的思路是:7 \to 3 \to 1 \to 0(每次去掉最高位的一个 1)。
这种做法的问题在于过早地引入了 0。一旦排列中出现了 0,前缀 AND 的结果就会永久变成 0,后面的数字无论是什么,贡献都是 0。
举个例子 (n=3):
-
你的构造:
7, 3, 1, 0, 2, 4, 5, 6-
前缀 AND:7, 3, 1, 0, 0, 0, 0, 0
-
Popcount:3 + 2 + 1 + 0 + \dots = 6
-
-
更优构造:
7, 3, 1, 5, 0, 2, 4, 6-
前缀 AND:
-
7 \ (111) \to pop 3
-
3 \ (011) \to 与上一个 111 还是 011,pop 2
-
1 \ (001) \to 与上一个 011 还是 001,pop 1
-
5 \ (101) \to 与上一个 001 AND 得到 001,pop 还是 1(这是关键!虽然 5 没有 bit 1,但它有 bit 0,延续了 bit 0 的寿命)
-
0 \ (000) \to pop 0
-
-
总分:3 + 2 + 1 + 1 + 0 \dots = 7
-
正确的解题思路(分治/位保留策略)
为了让分数最大,我们需要让每一个二进制位尽可能长时间地保持为 1。
为了让字典序最小,我们在选择“保留哪一位”时,应该优先保留低位(LSB),因为保留低位(如 bit 0)允许我们使用较小的奇数(1, 3...),而保留高位(如 bit n-1)会强制我们使用较大的数(2^{n-1} \dots)。
构造策略如下:
-
第一个数必须是 2^n - 1(全 1),以开启最大分数。
-
接下来,我们按照“倒序剥离”的方式来生成序列。
-
所有的数可以分为两类:bit \ 0 是 1 的,和 bit \ 0 是 0 的。
-
为了让 bit \ 0 活得最久(贡献最大),我们先把所有 bit \ 0 为 1 的数放前面(递归处理),等这些都放完了,再放 bit \ 0 为 0 的数。
-
在“bit \ 0 为 1”的这一组里,我们再把 bit \ 1 为 1 的放前面……
-
这实际上形成了一个特定的输出顺序。
-
直接构造算法(非递归版):
观察发现,最优且字典序最小的序列遵循以下规律:
-
先输出 2^n - 1。
-
从 i = n-1 循环到 0:
输出所有满足以下条件的数 x(按从小到大排序):
-
x 的第 0 到 i-1 位全是 1。
-
x 的第 i 位是 0。
-
x 的高于 i 的位任意。
-
这个构造保证了第 0 \sim i-1 位一直保持为 1,直到我们在这一轮把第 i 位变成 0。
优化后的代码
C++
// Problem: CF 2179 D
// Contest: Codeforces - Codeforces Round 1071 (Div. 3)
// URL: https://codeforces.com/contest/2179/problem/D
// Memory Limit: 256 MB
// Time Limit: 2000 ms
#include<bits/stdc++.h>
using namespace std;
#define ios ios::sync_with_stdio(false);cin.tie(nullptr);
void solve(){
int n;
cin >> n;
// 1. 首先输出全 1 的数
cout << (1 << n) - 1 << " ";
// 2. 按照层级倒序输出
// 每一层 i 代表:我们要"放弃"第 i 位了
// 在这之前,比 i 低的位 (0 到 i-1) 必须全是 1
for(int i = n - 1; i >= 0; i--){
// 后缀掩码:低 i 位全是 1
// 例如 i=2 (放弃bit 2), mask = (1<<2)-1 = 3 (二进制 011)
int suffix_mask = (1 << i) - 1;
// 这一层的数必须满足:
// (x & suffix_mask) == suffix_mask (低位全1)
// (x >> i) & 1 == 0 (第 i 位是 0)
// 我们遍历高位部分来生成这些数
// 高位部分共有 n - 1 - i 位
int high_bits_count = n - 1 - i;
int max_high = 1 << high_bits_count;
for(int h = 0; h < max_high; h++){
// 拼接:高位部分 + (0) + 低位部分
int val = (h << (i + 1)) | suffix_mask;
cout << val << " ";
}
}
cout << endl;
}
int main(){
ios;
int t;
cin >> t;
while(t--){
solve();
}
return 0;
}
代码解析 (n=3 的例子)
-
输出
7(111)。 -
循环 i=2 (放弃 bit 2):
-
要求:低 2 位是 1 (mask
011的低 2 位 ->x11? 不,是suffix_mask长度为 i)。 -
Wait, logic correction in explanation:
-
i=2: mask =
(1<<2)-1= 3 (011二进制). -
生成的数:Bit 2 必须是 0。低 0,1 位必须是 1。即
...011。 -
在这个循环里,
h从 0 遍历到 0。val = (0 << 3) | 3 = 3。 -
输出
3。
-
-
循环 i=1 (放弃 bit 1):
-
mask =
1(01二进制)。 -
生成的数:Bit 1 必须是 0。Bit 0 必须是 1。即
...01。 -
h遍历 0 到 1。 -
h=0:val = (0<<2) | 1 = 1。 -
h=1:val = (1<<2) | 1 = 5。 -
输出
1, 5。
-
-
循环 i=0 (放弃 bit 0):
-
mask =
0。 -
生成的数:Bit 0 必须是 0。即
...0。 -
h遍历 0 到 3。 -
0, 2, 4, 6。 -
输出
0, 2, 4, 6。
-
最终结果:7 3 1 5 0 2 4 6。这与我分析的最优解一致。
最终AC 代码
// Problem: CF 2179 D
// Contest: Codeforces - Codeforces Round 1071 (Div. 3)
// URL: https://codeforces.com/contest/2179/problem/D
// Memory Limit: 256 MB
// Time Limit: 2000 ms
#include<bits/stdc++.h>
using namespace std;
#define ios ios::sync_with_stdio(false);cin.tie(nullptr);
void solve(){
int n;
cin>>n;
cout<<(1<<n)-1<<" ";
for(int i=n-1;i>=0;i--){
int cur=(1<<i)-1;
int c=n-1-i;
int mx=1<<c;
for(int h=0;h<mx;h++){
int v=(h<<(i+1))|cur;
cout<<v<<" ";
}
}
cout<<endl;
}
int main(){
ios;
int t;
cin >> t;
while(t--){
solve();
}
return 0;
}