不喜欢特判?不喜欢分讨?不喜欢被卡 corner?不喜欢证明?不喜欢动脑子?
那就看这篇题解!
感性思路
首先感性地感受一下题目宽泛的限制条件题解区各种花式的构造方法就不难想出,符合条件的序列实在很多,那不是随便构造?但是随便上随机化还是很容易被卡而且常数太大,又不想写屎山分讨被 corner 卡到心态爆炸,怎么办!
爆搜!直接每一位枚举 \([1, n]\),先不说枚举到一个合法解的复杂度,朴素地检查需要把序列每一位都搜完,并且光检查就要 \(O(n^2)\)。我们需要将序列合法条件进行转换,最好做到若一位填的不合法立刻剪枝,就有希望很对!
首先 \(\sum\limits_{i=l}^{r} a_i = \sum\limits_{i=l}^{r} p_i\) 容易转换成 \(\sum\limits_{i=l}^{r} (a_i - p_i) = 0\),令 \(b_i = a_i - p_i\),那么这个条件就再次转换为序列 \(b\) 的前缀和序列没有两项是相同的(注意也不能为 0)。那么搜索的时候再记一个参数表示当前 \(a_i-p_i\) 的前缀和即可。注意出题人卡了一个 \(n \log n\),于是要用 unordered_set
检查。
理性证明
刚才只是感性理解了一下时间和做法,实际上这个时间是可以被证明的!
首先不难发现每次检查的都是一个前缀,那么也就是说如果前 \(i\) 个数合法,第 \(i + 1\) 个数没有冲突,那么前 \(i + 1\) 个数合法。搜索产生回溯当且仅当第 \(i + 1\) 个数与任意一个前缀都冲突。而随着 \(n\) 增大,前缀和的产生的值域范围也是非常大的,也就是不满的。可以证明 \(n\) 大于一个定值是一定不会产生回溯的。只有经过特殊构造的 \(n\) 很小的时候才会产生回溯。但是还有一个问题,无解会将搜索跑满,那时间复杂度就不对了?但注意无解是强于存在回溯的,也就是值域要求更加窄。可以证明,当且仅当 \(n=2\) 时才会产生无解。
还有一个好玩的性质,只要有解,那么必定能构造出一组使得 \(\forall p_i \leq 4\)。综上所述,可以认为时间复杂度最坏为 \(O(Vn)\),其中 \(V\) 为构造一组合法的 \(p\) 要用到的最大的 \(p_i\),最大为常数 4。复杂度正确。
实现代码
#include <iostream>
#include <unordered_set>
using namespace std;
typedef long long ll;
int uread() {
char c = getchar();
while (c < '0' || c > '9') {
c = getchar();
}
int num = 0;
while (c >= '0' && c <= '9') {
num = (num << 1) + (num << 3) + (c ^ 48);
c = getchar();
}
return num;
}
const int N = 1e6 + 1;
int n;
int a[N], b[N];
unordered_set<ll> st;
bool dfs(int pos, ll sum) {
if (pos == n + 1) {
return true;
}
for (int i = 1; i <= n; ++i) {
if (a[pos] == i) {
continue;
}
ll now = sum + a[pos] - i;
if (st.count(now)) {
continue;
}
st.insert(now);
b[pos] = i;
if (dfs(pos + 1, now)) {
return true;
}
st.erase(now);
}
return false;
}
void solve() {
n = uread();
for (int i = 1; i <= n; ++i) {
a[i] = uread();
}
st.clear(); st.insert(0ll);
if (!dfs(1, 0ll)) {//或直接特判 n == 2
puts("-1");
return ;
}
for (int i = 1; i <= n; ++i) {
printf("%d ", b[i]);
}
putchar('\n');
}
int main(int argc, const char * argv[]) {
int T = uread();
while (T--) {
solve();
}
return 0;
}