CF1779F 题解
思路
说是树上背包吧,不太像;说不是吧,又有点像。不过最终还是觉得算吧,因为他的这类子树合并上去的思维像是树上背包,不过时间复杂度不是。
直接设 $f[u][i]$ 为 $u$ 这颗子树,要使异或和为 $i$ 的最小操作数,那么普通转移方程为:$$ f[u][i \oplus j]=\min(f[u][i \oplus j],f[u][i]+f[v][j]) $$ 当然还有,但是是要在子树大小为偶数且 $i$ 大于 $0$ 的情况下:$$ f[u][0]=\min(f[u][0],f[u][i]) $$
接下来是记录路径的问题 我们对于每个 $f[u][i]$ 都开个动态数组存 $u$ 的每个儿子转移过来那个位置的第二维(在我下面的代码里用 $j$ 表示),那么我们假设当前有一个 $u$ 和 $i$ ,那么我们将发现我们可以通过当前的 $i$ 异或上动态数组的当前儿子转移过来的那个 $j$ ,得到最后一个儿子合并上来前的新下标,由于 $i$ 不用了所以我们把他更新成那个新下标。最后记得特别注意要把第二类转移也要处理。
也许这时候你思考,那如果每个状态都开一个动态数组,空间会不会爆?那就继续往下看时空复杂度分析。
时空复杂度
时间复杂度 $O(n*w^2)$ ,其中 $w$ 为 $32$ (不难发现,异或和不会超过 $31$ ),其以树上背包的时间复杂度为主要。
空间 $O(n*w)$ , $w$ 同上。那个动态数组的话我们考虑对于每个节点,他只会在父节点的 $32$ 个动态数组里面出现一遍,所以还是 $O(n*w)$ 的。
代码
#include <bits/stdc++.h>
#define L(i, a, b) for(int i = a; i <= b; i++)
#define R(i, a, b) for(int i = a; i >= b; i--)
using namespace std;
const int N = 2e5 + 10;
int n, a[N], f[N][32], g[32], sz[N];
vector<int> ans, e[N], p[N][32];
void Dfs(int u, int pa){
L(i, 0, 31) f[u][i] = 1e9;
f[u][a[u]] = 0, sz[u] = 1;
L(id, 0, (int)e[u].size() - 1){
int v = e[u][id];
Dfs(v, u), sz[u] += sz[v];
L(i, 0, 31) g[i] = 1e9, p[u][i].push_back(-1);
L(i, 0, 31){
L(j, 0, 31){
if(g[i ^ j] > f[u][i] + f[v][j]){
g[i ^ j] = f[u][i] + f[v][j];
p[u][i ^ j][id] = j;
}
}
}
L(i, 0, 31) f[u][i] = g[i];
}
if(!(sz[u] & 1))
L(i, 1, 31)
f[u][0] = min(f[u][0], f[u][i] + 1);
}
void Get(int u, int k){
if(!e[u].size()) return;
if(!k && !(sz[u] & 1)){
L(i, 1, 31)
if(f[u][i] + 1 == f[u][k]){
ans.push_back(u);
Get(u, i); return;
}
}
R(i, (int)e[u].size() - 1, 0){
int v = e[u][i];
Get(v, p[u][k][i]);
k ^= p[u][k][i];
}
}
int main(){
scanf("%d", &n);
L(i, 1, n) scanf("%d", &a[i]);
L(i, 2, n){
int p; scanf("%d", &p);
e[p].push_back(i);
}
Dfs(1, 0);
if(f[1][0] == 1e9){
puts("-1"); return 0;
}
printf("%d\n", f[1][0] + 1);
Get(1, 0);
reverse(ans.begin(), ans.end());
ans.push_back(1);
for(int u: ans) printf("%d ", u);
return 0;
}