E. Sending a Sequence Over the Network
题意
一个数列 a 可以构成若干个数列 b:
- 把数列 a 分成几段;
- 对于每一段,把其长度写到这一段的左边或者右边位置,构成数列 b。
例如把数列
a
=
[
1
,
2
,
3
,
1
,
2
,
3
]
a = [1, 2, 3, 1, 2, 3]
a=[1,2,3,1,2,3] 分成三段
[
1
]
+
[
2
,
3
,
1
]
+
[
2
,
3
]
[\color{red}{1}\color{black}] + \color{black}[\color{blue}{2, 3, 1}\color{black}] + [\color{green}{2, 3}\color{black}]
[1]+[2,3,1]+[2,3],可以构成以下四种数列 b:
b
=
[
1
,
1
,
3
,
2
,
3
,
1
,
2
,
3
,
2
]
b = [\color{black}1, \color{red}{1},\color{black}3, \color{blue}{2, 3, 1}, \color{green}{2, 3}, \color{black}2]
b=[1,1,3,2,3,1,2,3,2];
b
=
[
1
,
1
,
3
,
2
,
3
,
1
,
2
,
2
,
3
]
b = [\color{red}{1}, \color{black}1, 3, \color{blue}{2, 3, 1}, \color{black}2, \color{green}{2, 3}\color{black}]
b=[1,1,3,2,3,1,2,2,3];
b
=
[
1
,
1
,
2
,
3
,
1
,
3
,
2
,
2
,
3
]
b = [\color{red}{1}, \color{black}1, \color{blue}{2, 3, 1}, \color{black}3, 2, \color{green}{2, 3}\color{black}]
b=[1,1,2,3,1,3,2,2,3];
b
=
[
1
,
1
,
2
,
3
,
1
,
3
,
2
,
3
,
2
]
b = [\color{red}{1}, \color{black}1,\color{blue}{2, 3, 1}, \color{black}3, \color{green}{2, 3}, \color{black}2]
b=[1,1,2,3,1,3,2,3,2].
现在给出数列 b,判断其是否可以由一个数列 a 得到?
1 ≤ n ≤ 2 ⋅ 1 0 5 1 \le n \le 2 \cdot 10^5 1≤n≤2⋅105
思路
情况太复杂,考虑dp。
定义状态 f[i]
表示前 i 个位置是否合法。
对于当前 i 位置:
- 如果其作为每段右边的长度标志的话,那么可以从
i-a[i]-1
位置来转移。如果前i-a[i]-1
个位置是合法的,那么前i
个位置就合法。f[i] = max(f[i], f[i-a[i]-1];
- 如果其作为每段左边长度标志的话,那么可以对
i+a[i]
位置做贡献。如果前i-1
个位置合法的话,那么前i+a[i]
个位置就合法。f[i+a[i]] = max(f[i+a[i]], f[i-1]);
- 如果其不作为长度标志的话,那就应该往前遍历看哪一个是左边长度标志,从那个位置的前一个位置 x 来转移。但这样的时间复杂度就 n^2 了,显然不行。又转念一想,之前遍历到那个位置 x 的时候,其由于第二种操作已经把当前位置更新了,所以完全不用考虑这种情况。只考虑上述两种情况即可。
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N], f[N];
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i], f[i] = 0;
f[0] = 1;
for(int i=1;i<=n;i++)
{
if(i >= a[i] + 1) f[i] = max(f[i], f[i-a[i]-1]); //当前位置作为右边长度标志
if(i + a[i] <= n) f[i + a[i]] = max(f[i + a[i]], f[i-1]); //当前位置作为左边长度标志,对后面的位置产生贡献
}
if(f[n]) cout << "YES\n";
else cout << "NO\n";
}
return 0;
}
幸好下午看了几篇 dp 博客,能够想到用 dp ~
D. Masha and a Beautiful Tree
题意
给定一个长度为 n 的全排列,保证 n 为 2 的幂次。
分别表示为一个深度为 n 的完全二叉树的叶子节点的点权。
每次操作可以选择一个节点,然后互换其左右子树。
问,最少需要多少次操作,能把整个全排列变成升序?不合法输出 -1。
1 ≤ n ≤ 262144 ,保证为 2 的幂次 1 \le n \le 262144,保证为 2 的幂次 1≤n≤262144,保证为2的幂次
思路
贪心,先把小子树的顺序改变好,然后去改变大子树的顺序。
先两个两个考虑,如果不是升序的话,互换一次变成升序。
然后把两个合并成为一个,下标为偶数的那个节点/2,然后再两个两个考虑,如果不是升序的话,互换一次变成升序。
一直合并,直到合并成一个。
如果发现考虑的两个不是相邻数字的话,那就不合法。
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
const int N = 300010, mod = 1e9+7;
int T, n, m;
int a[N], b[N];
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i];
if(n == 1){cout << 0 << endl; continue;}
int flag = 0, cnt = 0;
while(1)
{
for(int i=1;i<=n;i+=2)
{
if(abs(a[i] - a[i+1]) != 1){
flag = 1; break;
}
if(a[i] == a[i + 1] + 1) cnt ++, swap(a[i], a[i + 1]);
}
if(flag) break;
int t = 0;
for(int i=1;i<=n;i+=2)
{
t ++;
b[t] = a[i+1]/2;
}
n = t;
for(int i=1;i<=n;i++) a[i] = b[i];
if(n == 1) break;
}
if(flag) cout << - 1 << endl;
else cout << cnt << endl;
}
return 0;
}