比赛链接:https://atcoder.jp/contests/agc035/tasks

A. XOR Circle

题目链接:https://atcoder.jp/contests/agc035/tasks/agc035_a

题目大意:把 $n$ 个数字放在一个环上,满足任意相邻的三个数字 $A,B,C$ 都满足: $A\otimes C = B$ 。

做法

不妨假设前三个数字为 $A,B,A\otimes B$ ,那么我们可以得到后面的数字为:$A,B,A\otimes B…$ ,可以看到 $3$ 是这个圆的周期。

因此,如果 $n$ 是 $3$ 的倍数,就是有三组 $n$ 个数字且异或和为 $0$ 。

否则就是所有数字都是 $0$ 。

时间复杂度:$O(n\log{n})$ ,而且不难做到 $O(n)$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, a[N];
bool pd(int l, int r){
return a[l] == a[r];
}
bool solve(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i];
}
sort(a + 1, a + n + 1);
if(n % 3 != 0) return pd(1, n) && !a[1];
return pd(1, n / 3) && pd(n / 3 + 1, n / 3 * 2) && pd(n / 3 * 2 + 1, n) && (a[n / 3] ^ a[n / 3 * 2] ^ a[n]) == 0;
}
int main(){
if(solve()) cout << "Yes\n";
else cout << "No\n";
return 0;
}

B. Even Degrees

题目链接:https://atcoder.jp/contests/agc035/tasks/agc035_b

题目大意:给每条边定个向,满足每个点的出度为偶数。

题解

这个问题可以等价的转化为一个经典问题:能不能进行边匹配,满足匹配边之间有一个公共点。(无重边)

这个问题有解的条件是:连通且边数为偶数。

做法是:把 $DFS$ 树建出来,然后把所有返祖边存在子树内,然后从下到上,依次把每个点的非父亲边两两匹配,如果剩下一条就和父亲边匹配,如果没有,就把父亲边像返祖边一样丢给父亲,显然在边数为偶数的情况下,这样一定能匹配完所有的边。

时间复杂度:$O(n)$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct node{
int y, next;
}a[N * 2]; int len, las[N];
void ins(int x, int y){
a[++len] = {y, las[x]};
las[x] = len;
}
vector<int> gfa[N];
int fa[N], dfn[N], ti;
int n, m, sta[N], top;
void dfs(int x){
dfn[x] = ++ti;
sta[++top] = x;
for(int k = las[x]; k; k = a[k].next){
int y = a[k].y;
if(y == fa[x]) continue;
if(dfn[y] && dfn[y] < dfn[x]) gfa[x].push_back(y);
else if(!dfn[y]){
fa[y] = x;
dfs(y);
}
}
}
void print(int x, int y){
cout << x << " " << y << "\n";
}
int main(){
cin >> n >> m;
if(m & 1){
cout << "-1\n";
return 0;
}
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
ins(x, y);
ins(y, x);
}
dfs(1);
for(int i = 1; i <= n; i++){
while(gfa[i].size() >= 2){
int x = gfa[i].back();
gfa[i].pop_back();
int y = gfa[i].back();
gfa[i].pop_back();
print(i, x);
print(i, y);
}
}
for(int i = n; i >= 1; i--){
int now = sta[i];
while(gfa[now].size() >= 2){
int x = gfa[now].back();
gfa[now].pop_back();
int y = gfa[now].back();
gfa[now].pop_back();
print(now, x);
print(now, y);
}
if(!gfa[now].size()) gfa[fa[now]].push_back(now);
else print(now, gfa[now][0]), print(now, fa[now]);
}
return 0;
}

update :

在看完题解后,感觉自己像个小丑,我这个做法完全可以这么翻译:

我在 $DFS$ 树上给非树边定向后,从下到上,对于每个点,如果出边为奇数,那么就把父亲边调转,何必多次一举去做匹配的过程。

或者说可以这么考虑,发现匹配过程得到的匹配对是完全没有用的,我们完全可以假装我们匹配过了,然后直接把点的所有非父亲边弹出,并根据数量决定需不需要父亲边。

还是写复杂了。

C. Skolem XOR Tree

题目链接:https://atcoder.jp/contests/agc035/tasks/agc035_c

题目大意:给你 $2n$ 个点,$i,i+n$ 的权值都是 $n$ ,要求你构造一棵树,满足 $i,i+n$ 路径(包括端点)的异或和为 $i$ 。

做法

不懂,感觉现在不会构造题啊,比赛时被这道题卡了一路。

首先注意到一个事情:$n=2^n$ 无解。

剩下的情况,如果 $n$ 是奇数的话,我们注意到可以:

$1$ 放中间,然后 $2k,2k+1$ 这样子放:$2k, 2k+1, 1, 2k, 2k+1$ 。

而 $1$ 的匹配可以随便找一个 $2k,2k+1$ 放在其屁股后面。

如果 $n$ 是偶数,那么假设 $n = k + t$ ,其中 $k,t$ 的或为 $0$ ,那么可以通过走 $k,k+1$ 和 $t,t+1$ 的路来跑出 $n$ 的路。

时间复杂度:$O(n)$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<bits/stdc++.h>
using namespace std;
int n;
int lowbit(int x){return x & -x;}
void print(int x, int y){
cout << x << " " << y << "\n";
}
int main(){
cin >> n;
if(lowbit(n) == n){
cout << "No\n";
return 0;
}
cout << "Yes\n";
for(int i = 3; i <= n; i += 2){
print(i - 1, i);
print(i, 1);
print(1, i - 1 + n);
print(i - 1 + n, i + n);
}
print(n + 1, n + 3);
if(n % 2 == 0){
int x = lowbit(n);
int y = n ^ x;
print(n, x + 1);
print(n + n, y + n);
}
return 0;
}

D. Add and Remove

题目链接:https://atcoder.jp/contests/agc035/tasks/agc035_d

题目大意:给你 $n$ 个数字,每次可以选择相邻的三个数字,把中间的数字删掉并且加到旁边两个数字,问最后剩下的两个数字之和的最小值。

我的做法

一种很新的状压 $dp$ 。

首先不大可能同时考虑整个序列,毕竟被删的位置没法决定当前的状态,和删除的顺序也有关,因此逐个位置考虑。

发现一个事情,一个数字删除后在的位置一定是相邻的两个格子,而其对最终答案的贡献取决于其这两个格子左右删除的顺序。

因此我们可以设一个 $dp[state]$ ,$state$ 表示当前这个数字对应的删除顺序。

然后直接转移就行了。

时间复杂度:$O(n2^n)$ 。

感觉这个状压 $dp$ 非常的有意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int B = 16;
LL dp[2][(1 << B)];
LL cnt[(1 << B)], a[B + 5];
int n;
int gethighbit(int x){return 31 - __builtin_clz(x);}
int getbit(int x, int y){return (x >> y) & 1;}
int changebit(int x, int y, int k){return x ^ ((getbit(x, y) ^ k) << y);}
int lowbit(int x){return x & -x;}
void init(){
cnt[1] = 2ll;
for(int i = 1; i < n; i++){
for(int x = 0; x < (1 << i); x++){
int tmp = x;
LL lc = 1ll, rc = 1ll;
for(int j = 0; j < i; j++){
int now = tmp & 1;
tmp >>= 1;
if(!now) rc = rc + lc;
else lc = lc + rc;
}
cnt[changebit(x, i, 1)] = lc + rc;
}
}
}
int main(){
cin >> n;
n -= 2;
init();
for(int i = 0; i <= n + 1; i++){
cin >> a[i];
}
if(n == 0){
cout << a[0] + a[1] << "\n";
return 0;
}
int now = 0, pre = 1;
memset(dp[now], 20, sizeof(dp[now]));
for(int i = 1; i <= n; i++) dp[now][(1 << i) - 1] = a[0] + a[n + 1] + cnt[(1 << i) - 1] * a[1];
for(int i = 2; i <= n; i++){
now ^= 1; pre ^= 1;
memset(dp[now], 20, sizeof(dp[now]));
for(int state = 1; state < (1 << (n - 1)); state++){
int nexstate = state << 1;
dp[now][nexstate] = min(dp[now][nexstate], dp[pre][state]);
}
for(int state = 1; state < (1 << (n - 1)); state++){
if(__builtin_popcount(state) > (n - i)) continue;
int nexstate = (state << 1) | 1;
dp[now][nexstate] = min(dp[now][nexstate], dp[now][state]);
}
for(int state = 1; state < (1 << n); state++){
int next1 = lowbit(state);
if(next1 != state){
next1 = __lg(next1);
int nexstate = (state >> (next1 + 1));
dp[now][nexstate] = min(dp[now][nexstate], dp[pre][state]);
}
}
for(int state = 1; state < (1 << n); state++){
dp[now][state] += a[i] * cnt[state];
}
}
LL ans = dp[now][0];
for(int i = 0; i < (1 << n); i++){
if(__builtin_popcount(i) == 1) ans = min(ans, dp[now][i]);
}
cout << ans << "\n";
return 0;
}

但是这个做法又可以优化的空间吗?

我们来精细计算一下:

对于一个 $k$ 位的状态,其有效的区间是 $n-k$ 。

因此实际上有效的状态为:$\sum\limits_{i=0}^n 2^{i}(n-i)=\sum\limits_{i=0}^{n-1}\sum\limits_{j=0}^i2^{j}=\sum\limits_{i=0}^{n-1}2^{i+1}-1=2^{n+1}-(n+2)$ 。

所以如果在 $dp$ 的时候只考虑有效状态的话,实际上是 $O(2^n)$ 的。

能够实现出来吗?$cnt$ 的计算使用位运算技巧可以做到 $2^n$ ,而 $dp$ 如果使用 vector 提前把所有可行状态存下来,也同样可以做到 $O(状态数)$,也就是 $O(2^n)$ 。

因此,上述 $dp$ 完全可以做到 $O(2^n)$ 的空间和时间,但是会比较难写。

对此我的评价是,完全不如正解。

正解

有一种形式的 $dp$ 非常的好写,相较于其余的 $dp$ 。

就是记忆化形式的 $dp$ ,甚至有的时候可以去掉记忆化,那样就更好写了。

首先把 $a_1,a_n$ 给删掉,然后在数组两边放上两个变量 $L,R$ ,这样就变成了每次删除一个数字,然后把这个数字给放到两边,这样删完这个数组后得到的答案就是 $L+R$ 。

因此,倒着考虑每个区间最后被删除的数字是啥,我们就可以得到如下的 dp :

$dp[l][r][cl][cr]$ 表示 $[l,r]$ 区间中的数字删完后,贡献为 $Lcl+Rcr$ 后的贡献最小值。

然后 $dp[l][r][cl][cr]=\min\limits_{l\le i\le r}(dp[l][i-1][cl][cl+cr]+dp[i+1][r][cl+cr][cr]+(cl+cr)*a[i])$ 。

然后用这个式子 $dp$ 就行了。

最后答案就是 $dp[1][n][1][1]+a[1]+a[n]$ 。

那么状态是多少呢?注意到这个的状态数计算和我的做法一样,都是 $\sum\limits_{i=0}^{n}2^{i}(n-i)$ ,也就是 $O(2^n)$ 。

而合法的转移数是 $\sum\limits_{i=0}^{n}2^{i}(n-i)^2$ ,也是 $O(2^n)$ 的.

注:$\sum\limits_{i=0}^{n}2^{i}(n-i)^k$ 都是 $O(2^n)$ 的。

因此如果使用记忆化的话时间复杂度为:$O(2^n*T)$ ,$T$ 为使用的数据结构的储存时间。

但是使用数据结构储存还是麻烦了,如果使用 map 还得自己写个四元组作为键。(貌似也可以使用 array

因此不妨考虑不记忆化,那么不记忆化的复杂度是多少呢?

设 $T(n)$ 表示一个长度为 $r-l+1=n$ 的递归复杂度,那么 $T(n)$ 满足:

$T(n)=\sum\limits_{i=0}^{n-1}2T(i)$

那么可以得到 $T(n)=3T(n-1)$ ,从而知道复杂度就是 $O(3^n)$ 。

这样代码就好写很多了。

这里粘一份别人的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <algorithm>
using namespace std;
long long a[25];
long long dfs(int l,int r,int xl,int xr)
{
if(r-l<=1)return 0;
long long ans=1e18;
for(int i=l+1;i<=r-1;i++)
ans=min(ans,dfs(l,i,xl,xl+xr)+dfs(i,r,xl+xr,xr)+a[i]*(xl+xr));
return ans;
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
cout<<a[1]+a[n]+dfs(1,n,1,1)<<endl;
return 0;
}
//代码来源:https://www.luogu.com.cn/article/nuav36aw
//代码作者:StudyingFather

怎么能想出这么好写的做法?

我不知道,我没想出来。

不过我觉得答案应该是:积累。