T1 Odd Subarrays
来将题意简单转化一下:
将 1 ∼ n 1∼n 1∼n的一个排列1分划成若干连续子序列,使得逆序对对数为奇数的子序列数量最多。
我们考虑其中一个数 a i a_i ai以及一个最小的满足 a j < a i a_j<a_i aj<ai的 j , j, j,现在考虑区间 ( i , j ) 。 (i,j)。 (i,j)。显然,对于任意的 i < k < j i<k<j i<k<j必有 ( a k , a j ) (a_k,a_j) (ak,aj)是一对逆序对。此时,将该区间进一步划分为 ( i , j − 2 ) (i,j-2) (i,j−2)和 ( j − 1 , j ) (j-1,j) (j−1,j),则 ( j − 1 , j ) (j-1,j) (j−1,j)必然是一个逆序对数为奇数的子序列,即这样划分两个区间后,逆序对对数为奇数的子序列数一定比之前划分的多。因此,我们最终只需要统计满足 a i ? a i + 1 a_i?a_{i+1} ai?ai+1的配对个数即可。
c o d e code code
#include<bits/stdc++.h>
using namespace std;
int t,n,p[100005],ans;
int main(){
cin>>t;
while(t--){
cin>>n;
ans=0;
for(int i=0;i<n;i++)cin>>p[i];
for(int i=0;i<n-1;i++){
if(p[i]>p[i+1]){
ans++;
i++;//防止重复配对
}
}
cout<<ans<<endl;
}
return 0;
}
记不清楚顺序了,就一道一道来了。
AT_arc174_f [ARC174F] Final Stage
这道是黑题啊,畏惧了。但也就是dp+堆
还是一样的步骤,先来简化一下题意:
有一堆石子,两个人轮流取石子,共取 n n n次,第 i i i次要取 [ l i , r i ] [l_i,r_i] [li,ri]个石子,无法操作的人输, q q q次询问如果初始有 c c c个石子,谁会获胜。
来想想思路:
如果 n = 1 n=1 n=1,那么 [ 0 , l i ) [0,l_i) [0,li)必败, [ l i , + ∞ ) [l_i,+\infty ) [li,+∞)平局。
然后考虑加入 [ l n − 1 , r n − 1 ] [l_{n-1},r_{n-1}] [ln−1,rn−1],此时对于每个必败区间 [ x , y ] [x,y] [x,y],其都会变成 [ x + l n − 1 , y + r n − 1 ] [x+l_{n-1},y+r_{n-1}] [x+ln−1,y+rn−1]并取并,然后翻转必胜必败情况,最后设 [ 0 , 0 ] [0,0] [0,0]必败。
我们要动态维护这些操作,可以只维护所有差分位置,即 c = i − 1 c=i-1 c=i−1与 c = i c=i c=i时结果不同的位置。
那么奇数项差分位置就是必败区间的左端点,偶数项差分位置就是必败区间的右端点。
给不同奇偶的差分位置分别加上 l n − 1 / r n − 1 l_{n-1}/r_{n-1} ln−1/rn−1,如果两个差分位置相交,说明两个必败区间相交,把这两个差分位置都删除。
用堆维护所有奇偶差分位置的距离,每次把会相遇的差分位置弹出并暴力删除即可,双向链表维护所有差分位置即可。
c o d e code code
#include<bits/stdc++.h>
#define ll long long
#define fi first
#define se second
using namespace std;
ll read() {
ll x = 0, f = 1;
char ch = getchar();
while (ch > '9' || ch < '0') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
const int MAXN = 3e5 + 5;
ll l[MAXN], r[MAXN], x[MAXN];
ll tl, tr;
ll ans[MAXN];
int n, q;
int lr[MAXN], ls[MAXN];
set <pair<ll, int>> ql, qr;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
l[i] = read(), r[i] = read();
}
int hd = 1, tot = 2;
ls[1] = 2, lr[2] = 1, x[2] = l[n];
ql.insert({l[n], 1});
for (int k = n - 1; k; --k) {
tl += l[k], tr += r[k];
while (qr.size() && qr.begin()->fi <= tr - tl) {
int i = qr.begin()->second, j = ls[i];
qr.erase(qr.begin());
if (!ls[j]) {
ls[i] = 0;
continue;
}
if (!lr[i]){
hd = ls[j];
}
ls[lr[i]] = ls[j];
lr[ls[j]] = lr[i];
if (lr[i]){
ql.erase({x[i] - x[lr[i]], lr[i]});
}
if (ls[j]){
ql.erase({x[ls[j]] - x[j], j});
}
if (lr[i] && ls[j]){
ql.insert({x[ls[j]] - x[lr[i]], lr[i]});
}
}
swap(tl, tr);
swap(ql, qr);
++tot, lr[hd] = tot, ls[tot] = hd;
x[tot] = -tl, ql.insert({x[hd] - x[tot], tot}), hd = tot;
}
int m = 0;
for (int i = hd; i; i = ls[i]) {
++m;
if (m & 1) ans[m] = x[i] + tl;
else ans[m] = x[i] + tr;
}
scanf("%d",&q);
while (q--) {
ll z;
z = read();
int i = upper_bound(ans + 1, ans + m + 1, z) - ans - 1;
if (i == m) puts("Draw");
else {
if (i & 1) puts("Bob");
else puts("Alice");
}
}
return 0;
}
T3 Range Sorting (Hard Version)
这道题还行,在luogu交了RE了4个点,在原题交没事。
一样的思路,简化题意:
给定长度为n的数组a,n个元素两两不同。 1 ≤ n ≤ 3 ∗ 1 0 5 , 1 ≤ a i ≤ 1 0 9 1\le n\le{3*10^5},1\le{a_i}\le{10^9} 1≤n≤3∗105,1≤ai≤109。
定义一个数组 p 1 , p 2 , . . . , p k p1,p2,...,pk p1,p2,...,pk的美丽值为,将它升序排序所需要的最小开销。 每次操作,可以选择一个区间, [ l , r ] [l,r] [l,r]并将数组的区间 [ l , r ] [l,r] [l,r]进行升序排序。对应端开销为 [ r − l ] [r-l] [r−l]。
给定数组a,求它所有连续子数组的美丽值之和。
定义数组a的连续子数组为,选择下标 1 ≤ l ≤ r ≤ n 1\le{l}\le{r}\le{n} 1≤l≤r≤n后,截取的子数组 a [ l ] , a [ l + 1 ] , . . . , a [ r ] 。 a[l],a[l+1],...,a[r]。 a[l],a[l+1],...,a[r]。
考虑极端情况,整个数组都是逆序的。
n
,
n
−
1
,
n
−
2
,
.
.
.
,
2
,
1
n, n-1, n-2, ... , 2, 1
n,n−1,n−2,...,2,1
那么这时候,所有连续子数组的美丽值之和,就为
res = 0;
for i = 1 to n:
tmp = 0 + 1 + 2 + ... + i-1 = i * (i - 1) / 2;
res += tmp;
我们可以先假设所有子区间需要翻转。再减去实际上为正序的区间。 我们可以枚举每个下标位置,分别求出小于 a [ i ] a[i] a[i]的左区间,以及大于 a [ i ] a[i] a[i]的右区间。这些即为正序的区间。
c o d e code code
//stl+dp
//https://codeforces.com/problemset/problem/1827/B2
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read() {
int x = 0, f = 1;
char ch = getchar();
while (ch > '9' || ch < '0') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
void write(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
const int N = 500005;
const int M = 19;//区间长度
int a[N], r[N];
int f[M][N];
//f[k][i]表示 以i为右边界,i - (1<<k) + 1为 左边界的 区间最大值
void solve() {
int n;
n = read();
int res = 0;
vector<int> s;//stl大法好
a[0] = a[n + 1] = 0;
for (int i = 1; i <= n; i++) {
a[i] = read();
f[0][i] = a[i];//头尾都初始化为0
// 假设所有区间都需要翻转,计算所有所有的区间和
res += 1ll * i * (i - 1) / 2;
}//右区间入队
s.push_back(n + 1);
for (int i = n; i > 0; i--) {
while (a[s.back()] > a[i]) {// 单调栈,用于求r[i]
s.pop_back();
}
r[i] = s.back(); // r[i]记录 从下标i开始,不小于i的元素的 右边界
s.push_back(i);
}
for (int i = 1; i < M; i++) {// f算法求区间,最大值
for (int j = (1 << i); j <= n; j++) {
f[i][j] = max(f[i - 1][j], f[i - 1][j - (1 << (i - 1))]);
}
}
s.clear();
s.push_back(0);
for (int i = 1; i <= n; i++) {
while (a[s.back()] > a[i]) {// 单调栈,用于求s.back()
s.pop_back();
}
// s.back() 表示 从下标i开始,小于i的元素的起始右边界
// j表示 从下标s.back() 开始, [j,s.back()]区间最大值 < a[i]的最左边界
int j = s.back();
for (int k = M - 1; k >= 0; k--) {
if (j >= (1 << k) && f[k][j] < a[i]) {
j -= 1 << k;
}
}
//记得long long
res -= 1ll * (r[i] - i) * (s.back() - j); // 减去不需要翻转的区间
s.push_back(i);
}
write(res);
cout << endl;
}
signed main() {
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
T4 AT_abc337_g [ABC337G] Tree Inversion
原题链接
蒟蒻(ju ruo)
逆天,这道题竟然用到了换根dp知识点,没事,继续学!
下面我们来了解换根dp的一些基础知识点:
换根dp一般可以将
O
(
n
2
)
O(n^2)
O(n2)的做法优化到线性
O
(
n
)
O(n)
O(n)。
换根dp分为3个步骤:
1.
1.
1. 先指定一个根节点。
2.
2.
2. 一次dfs统计子树内的节点对当前节点的贡献。
3.
3.
3. 一次dfs统计父亲节点 对当前节点的贡献并合并统计最终答案。
下面来步入正题:
既然知道了是换根dp,那我们如果用朴素的做法求解,时间复杂度就是 O ( n 2 ) O(n^2) O(n2)。
下面来讲一下换根dp的思路:
这个题发现就是求类似树上逆序对的个数,即在同一个子树里,节点编号和深度大小关系相反。考虑第一步,设
f
[
x
]
f[x]
f[x]表示以节点编号
x
x
x为根的子树包含逆序对的个数。显然,我们要知道子树中编号小于
x
x
x的节点数量
a
x
a_x
ax,那么状态转移方程就很显然了:
f [ x ] = a x + ∑ y ∈ s o n ( x ) f [ y ] f[x]=a_x+\sum_{y\in{son(x)}}f[y] f[x]=ax+∑y∈son(x)f[y]
接下来考虑第二部,设 g [ x ] g[x] g[x]表示以 x x x为根节点的树的逆序对的个数,思考由 x x x换根为 y y y后, g [ x ] g[x] g[x]通过什么样的转换关系到达 g [ y ] g[y] g[y],通过下面的图示来清楚看懂。
这里还需要添加一个
b
x
b_x
bx表示以
x
x
x为根的子树内编号小于
f
a
[
x
]
fa[x]
fa[x]的节点数量。
特别注:总的小于
y
y
y的节点个数是
y
−
1
y-1
y−1,已知
y
y
y子树内的数量有
a
y
a_y
ay,相减即子树外的部分
g
[
y
]
=
g
[
x
]
−
b
y
+
(
y
−
1
−
a
y
)
g[y]=g[x]-b_y+(y-1-a_y)
g[y]=g[x]−by+(y−1−ay)
下面求解 a x , a y a_x,a_y ax,ay,可以用树状数组,作差一下,用全局的值减掉子树外的值即可。
c o d e code code
#include <bits/stdc++.h>
#define re register int
#define lp p << 1
#define rp p << 1 | 1
#define int long long
using namespace std;
const int N = 2e5 + 10;
struct Edge {
int to, next;
} e[N << 1];
int idx, h[N];
struct Tree {
int l, r, sum;
} t[N << 2];
int n, f[N], g[N], a[N], b[N];
inline void add(int x, int y) {
e[++ idx] = (Edge) {
y, h[x]
};
h[x] = idx;
}
inline void build(int p, int l, int r) {
t[p].l = l, t[p].r = r;
if (l == r) return;
int mid = (l + r) >> 1;
build(lp, l, mid);
build(rp, mid + 1, r);
}
inline void update(int p, int x, int k) {
if (t[p].l == x && t[p].r == x) {
t[p].sum += k;
return;
}
int mid = (t[p].l + t[p].r) >> 1;
if (x <= mid) update(lp, x, k);
if (x > mid) update(rp, x, k);
t[p].sum = t[lp].sum + t[rp].sum;
}
inline int query(int p, int l, int r) {
if (l > r) return 0;
if (l <= t[p].l && t[p].r <= r) return t[p].sum;
int res = 0;
int mid = (t[p].l + t[p].r) >> 1;
if (l <= mid) res += query(lp, l, r);
if (r > mid) res += query(rp, l, r);
return res;
}
void dfs1(int u, int fa) {
a[u] = -query(1, 1, u - 1);
b[u] = -query(1, 1, fa - 1);
for (re i = h[u]; i; i = e[i].next) {
int v = e[i].to;
if (v == fa) continue;
dfs1(v, u);
}
update(1, u, 1);
a[u] += query(1, 1, u - 1);
b[u] += query(1, 1, fa - 1);
f[u] = a[u];
for (re i = h[u]; i; i = e[i].next) {
int v = e[i].to;
if (v == fa) continue;
f[u] += f[v];
}
}
void dfs2(int u, int fa) {
if (u != 1)
g[u] = g[fa] - b[u] + (u - 1 - a[u]);
for (re i = h[u]; i; i = e[i].next) {
int v = e[i].to;
if (v == fa) continue;
dfs2(v, u);
}
}
signed main() {
cin >> n;
for (re i = 1; i < n; i ++) {
int x, y;
cin >> x >> y;
add(x, y), add(y, x);
}
build(1, 1, n);
dfs1(1, 0);
g[1] = f[1];
dfs2(1, 0);
for (re i = 1; i <= n; i ++) {
cout << g[i] << ' ';
}
cout << '\n';
return 0;
}
T5 P9726 [EC Final 2022] Magic
这道题用到了网络流,不会网络流的看这里(网络流保姆级教程)我也刚又看一遍,才彻底通透。这道题还用到了dinic(点这里)
好了,前缀知识已经介绍差不多了,下面开始步入正题。
该说不说,道题是我感觉今天的题里除了国家集训队的题最难的一道了。
换⼀种⽅式考虑:
要选择若⼲个位置,使得这些位置最后贡献给答案,要求合法且选择的位置数
量最多。
对于区间
[
l
1
,
r
1
]
和
[
l
2
,
r
2
]
,
[l_1, r_1] 和 [l_2, r_2],
[l1,r1]和[l2,r2],若
l
1
<
l
2
<
r
1
<
r
2
l_1 < l_2 < r_1 < r_2
l1<l2<r1<r2,那么如果选择
l
2
l_2
l2,就意味着
[
l
1
,
r
1
]
[l_1, r_1]
[l1,r1]
要在
[
l
2
,
r
2
]
[l_2, r_2]
[l2,r2] 之前执⾏;同理如果选择
r
1
r_1
r1,就意味着
[
l
2
,
r
2
]
[l_2, r_2]
[l2,r2] 要在
[
l
1
,
r
1
]
[l_1, r_1]
[l1,r1] 之前执⾏。
所以
r
1
r_1
r1 与
l
2
l_2
l2不能同时选择,所以⼀定是⼀个建⽆向图后的独⽴集。
下面来证明一下这个结论是合法的:
只要选择独⽴集后不会成环,那么就能够说明这是合法的。
如果成环,假设环上选了⼀个区间
[
l
,
r
]
[l,r]
[l,r] 的右端点
r
r
r,导致它⽐某个区间
[
l
′
,
r
′
]
[l',r']
[l′,r′] 要
晚执⾏
(
l
<
l
′
<
r
<
r
′
)
。
(l < l' < r < r')。
(l<l′<r<r′)。
由于
l
′
l'
l′ 不能被选择,那么下⼀个环上的点⼀定是由于选择了
r
′
r'
r′ 导致的。
那么以此类推,在这条链上的区间
r
r
r ⼀定单调递增,所以不可能成环。
所以问题转化为了最⼤独⽴集问题。
⼜由于只有左端点和右端点之间的边,这是⼀个⼆分图最⼤独⽴集问题。
bitset 优化匈⽛利:
O
(
n
3
/
w
)
。
O(n3/w)。
O(n3/w)。
可持久化线段树优化建图 dinic:
O
(
n
2
l
o
g
n
)
O(n2log n)
O(n2logn)
这就是这道题的实现思路了,代码里还有一些细节需要注意,自行观看。
c
o
d
e
code
code
#include<bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define ll long long
using namespace std;
const int N = 5005;
int n, tot, l[N], r[N], match[N];
int px[N], py[N];
bitset<N>to[N], vs;
queue<int>Q;
int bfs(int u) {
while (Q.size())Q.pop();
vs.set(), Q.push(u);
int v = -1;
while (Q.size()) {
int x = Q.front();
Q.pop();
bitset<N>tmp = vs & to[x];
for (int y = tmp._Find_first(); y <= n; y = tmp._Find_next(y)) {
int z = match[y];
vs[y] = 0;
if (z == 0) {
match[y] = x, v = x;
break;
}
Q.push(z), px[z] = x, py[z] = y;
}
if (~v)break;
}
if (v == -1)return 0;
while (v != u) {
match[py[v]] = px[v];
v = px[v];
}
return 1;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> l[i] >> r[i];
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (l[i] < l[j] && l[j] < r[i] && r[i] < r[j]) {
to[i][j] = 1;
}
}
}
for (int i = 1; i <= n; i++) {
tot += bfs(i);
}
cout << 2 * n - tot;
}
总结
出的题很有难度,除了T1 ,T2 都不会,思路都不咋有,基本上都是一些算法的整合,综合运用,但还是有一些新的东西,比如换根dp之类的,那是真的不会。