Conclusion
D题用到了“对比前后变化”的思想来处理一些看起来比较复杂的东东
A - Reachable Towns
介绍两种做法,一种是比较好理解的并查集,一种是奇奇怪怪的(O(n))做法
并查集做法:
最naive的想法就是在每两个符合条件的点之间连边。但事实上确保联通性最多只要(n)条边,思考一下哪些边是必须的
先按照横坐标排序,假设当前处理到(i),之前已经构成了一些并查集(每个并查集内的点相互联通),记录一下每个并查集内最小的值(mn_j)
如果({y_i}>{mn_j}),就可以将(i)和集合(j)进行(merge)。可以用单调栈按照最小值维护每个并查集,只会进行(n)次(merge)
#include <bits/stdc++.h>
using namespace std;
void read (int &x) {
char ch = getchar(); x = 0;
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
} const int N = 2e5 + 5;
int n, a[N], x[N];
int fa[N], mn[N], tp, st[N], sz[N];
int find (int x) {
return fa[x] == x ? x : (fa[x] = find (fa[x]));
}
void merge (int x, int y) {
int fx = find (x), fy = find (y);
if (fx != fy) {
fa[fx] = fy, mn[fy] = min (mn[fy], mn[fx]), sz[fy] += sz[fx];
}
}
signed main() {
read (n);
for (int i = 1, y; i <= n; ++i)
read (x[i]), read (y), a[x[i]] = y;
for (int i = 1; i <= n; ++i)
fa[i] = i, mn[i] = a[i], sz[i] = 1;
for (int i = 1; i <= n; ++i) {
while (tp && a[i] > mn[find (st[tp])])
merge (st[tp], i), --tp;
st[++tp] = find (i);
}
for (int i = 1; i <= n; ++i)
printf ("%d
", sz[find (x[i])]);
return 0;
}
2、奇怪的(O(n))做法
先放代码然后给证明
#include <bits/stdc++.h>
using namespace std;
void read (int &x) {
char ch = getchar(); x = 0;
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
} const int N = 2e5 + 5;
int n, a[N], x[N], res[N], c, t[N];
signed main() {
read (n);
for (int i = 1, y; i <= n; ++i)
read (x[i]), read (y), a[x[i]] = y;
int m = n + 1;
for (int i = 1; i <= n; ++i) {
m = min (m, a[i]);
if (m == n - i + 1) t[++c] = i;
}
for (int i = 1; i <= c; ++i)
for (int j = t[i]; j > t[i - 1]; --j)
res[j] = t[i] - t[i - 1];
for (int i = 1; i <= n; ++i) printf ("%d
", res[x[i]]);
return 0;
}
意思就是先按(x)排序,找到所有(y)前缀最小值等于后面一段长度的位置,然后相邻两个位置之间的所有答案都是两个位置的差值
先定义一个数组(q={n,n-1,n-2,...,1}),其实满足条件"前缀最小值等于后面一段长度"的位置(k)等价于:
(y)的(1—k)的子序列是(q)的(1—k)的子序列的一个排列
假设现在有了两个相邻的这样的位置(l,r),根据了上面的结论,(y)的(l+1—r)的子序列就是(q)的(l+1—r)的子序列的一个排列
不难发现,这段内的所有位置无法到达(1—l)和(r+1—n),所以现在只需证明(l+1—r)的所有位置可以相互到达
引理:1、如果从位置(a)可以一步走到(b)((a<b)),那([a,b])区间内都可以相互走到
2、如果从(a_i)可以一步走到(b_i),设(s)为所有区间([{a_i},{b_i}])的并集,则(s)中的所有位置都可以相互走到
应该不难证明
因为(l+1—r)内没有符合条件的位置,所以任选一个位置(win [l+1,r-1]),(l+1-w)中至少有一个数在({{q_{w+1}}-{q_r}})中,(w+1—r)中至少有一个数在({{q_{l+1}}—{q_w}})中
也就是说,至少存在两个位置(ain [l+1,w],bin [w+1,r]),从(a)可以一步到(b),而所有(w)的([{a_w},{b_w}])的并集一定是([l+1,r]),根据引理2,([l+1,r])中的数都可以相互走到,所以这一段的所有答案都为(r-l)
B - Sum is Multiple
根据求和公式,再把(/2)移过去得到:(n imes2 | k imes(k+1)),下面的(n)指的都是(n imes2)
假设(s=k imes(k+1)),质因数分解后(n=sum{ {p_i} ^ {k_i}}),(s=sum{ {p_j} ^ {k_j}})
若要满足上述条件,则({forall}i,{p_i} ^ {k_i}|s)
观察到(k)和(k+1)互质,而要满足上述条件,则一部分的({p_i}^{k_i}|k),另一部分({p_i}^{k_i}|(k+1))
枚举方案(a,b)使得(a imes b=n,gcd(a,b)=1),(k=x imes a, (k+1)=y imes b),即(y imes b - x imes a = 1)
用exgcd求出解,缩小到最小就ok了
复杂度为(O({2^{num}} imes log_n)),(num)为(n)的约数个数,因该不会超过(20)个吧
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 5;
int n, res, p[N], c;
int exgcd (int a, int b, int &x, int &y) {
if (!b) { x = 1, y = 0; return a; }
int d = exgcd (b, a % b, x, y);
int z = x; x = y; y = z - y * (a / b);
return d;
}
void update (int a, int b) {
int x, y; exgcd (a, b, x, y);
int k = min (x / b, y / a);
x -= k * b, y += k * a;
while (x <= 0) x += b, y -= a;
while (y >= 0) y -= a, x += b;
if (x > 0 && y < 0) res = min (res, x * a - 1);
}
void solve (int x, int sum, int k) {
if (k) update (sum, n / sum);
if (x > c) return;
solve (x + 1, sum * p[x], 1);
solve (x + 1, sum, 0);
}
signed main() {
scanf ("%lld", &n);
if (n == 1) return puts ("1"), 0;
res = n - 1; n <<= 1; int t = n;
for (int i = 2; i * i <= t; ++i) {
if (t % i == 0) {
p[++c] = 1;
while (t % i == 0)
p[c] *= i, t /= i;
}
} if (t > 1) p[++c] = t;
solve (1, 1, 1);
return printf ("%lld
", res), 0;
}
C - Moving Pieces
一个球对应一个位置,每个位置只能放一个球,有点类似二分图,用网络流求解
连边方式很多,这里介绍边数量是(n imes m)的方法
从((i,j))走到((x,y))的步数是(x+y-i-j),先把所有出发点的横纵坐标减去(把后面的(-i-j)算掉)
1、因为只能向右向下走,把每个格子当作一个点,在((x,y))与((x,y+1))和((x+1,y))之间连边,每个格子可以走过多次,流量为无限大,价值为(0)
2、对于每个((x,y)=)'o',从源点到((x,y))连一条边,流量为(1),价值为(0)
3、对于每个点((x,y)),连一条从((x,y))到汇点的边,流量为(1),价值为(x+y),对应上边的式子
跑一边费用流求出答案
#include <bits/stdc++.h>
using namespace std;
void read (int &x) {
char ch = getchar(); x = 0;
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
} const int N = 55, M = N * N * 10, K = N * N;
int n, m, res, id[N][N]; char ch[N][N];
int cnt = 1, h[K], to[M], nxt[M], w[M], val[M];
void add (int u, int v, int l, int ww) {
to[++cnt] = v, val[cnt] = ww, w[cnt] = l, nxt[cnt] = h[u], h[u] = cnt;
to[++cnt] = u, val[cnt] = -ww, w[cnt] = 0, nxt[cnt] = h[v], h[v] = cnt;
} int s, t, in[K], d[K], vis[K], pre[K]; queue<int> q;
int spfa () {
while (!q.empty()) q.pop();
memset (d, 0xcf, sizeof (d));
memset (vis, 0, sizeof (vis));
q.push (s); d[s] = 0, vis[s] = 1; in[s] = 1e9;
while (!q.empty()) {
int u = q.front(); q.pop(); vis[u] = 0;
// printf ("%d %d
", u, d[u]);
for (int i = h[u], v; i; i = nxt[i])
if (w[i] && d[v = to[i]] < d[u] + val[i]) {
d[v] = d[u] + val[i];
in[v] = min (in[u], w[i]), pre[v] = i;
if (!vis[v]) vis[v] = 1, q.push (v);
}
}
return (d[t] > 0);
}
void update () {
int x = t;
while (x != s) {
int i = pre[x];
w[i] -= in[t], w[i ^ 1] += in[t];
x = to[i ^ 1];
} res += d[t];
}
signed main() {
read (n), read (m);
s = 0, t = n * m + 1;
for (int i = 1; i <= n; ++i)
scanf ("%s", ch[i] + 1);
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j)
id[i][j] = (i - 1) * m + j;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= m; ++j) {
if (ch[i][j] == '#') continue;
if (ch[i][j] == 'o')
res -= i + j, add (s, id[i][j], 1, 0);
add (id[i][j], t, 1, i + j);
if (i < n) add (id[i][j], id[i + 1][j], 1e9, 0);
if (j < m) add (id[i][j], id[i][j + 1], 1e9, 0);
}
while (spfa()) update();
return printf ("%d
", res), 0;
}
D - Keep Distances
暴力做法:
先对于每个询问区间(ql,qr),求出最大的子集大小:
从(ql)开始,每步走到最近的满足条件的数,直到超出范围,这样得出的就是最优解
答案要求的是所有集合的并,只要对每个数分别考虑是否存在一个最大的合法集合包含这个数
设从(ql)开始贪心得到的序列为(a),从(qr)往左走按照同样方法贪心得到的序列为(b)
答案就是(sum{{b_i}-{a_i}+1}),意思就是在每个([{a_i},{b_i}])内的数都可能存在于集合中,其他的不行
正确性比较好理解,(a)和(b)是分别从左右端开始的最优方案,相当于是一个上下界的限制,在里面的自然都可以
用倍增优化这个过程,以从左往右的为例,从右边开始的类似:
(l[i][j])表示从(j)开始走了(2^i)到达的位置。上述公式里用到下标的和,再用(L[i][j])表示从(j)开始走了(2^i)过程中走过的下标的和
预处理时间(O(nlog_n)),处理每次询问(O(log_n)),总复杂度(O((n+q)log_n))
#include <bits/stdc++.h>
using namespace std;
#define int long long
void read (int &x) {
char ch = getchar(); x = 0;
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
} const int N = 2e5 + 5;
int n, k, a[N], l[20][N], L[20][N], r[20][N], R[20][N], c[20];
signed main() {
read (n), read (k); c[0] = 1;
for (int i = 1; i < 18; ++i) c[i] = c[i - 1] << 1;
for (int i = 1; i <= n; ++i) read (a[i]);
for (int i = 0; i < 19; ++i) l[i][n + 1] = n + 1;
for (int i = 1; i <= n; ++i) {
l[0][i] = n + 1;
int ll = i + 1, rr = n, mid;
while (ll <= rr) {
mid = ll + rr >> 1;
if (a[mid] >= a[i] + k) l[0][i] = mid, rr = mid - 1;
else ll = mid + 1;
} L[0][i] = l[0][i];
}
for (int i = 1; i <= n; ++i) {
int ll = 1, rr = i - 1, mid;
while (ll <= rr) {
mid = ll + rr >> 1;
if (a[mid] <= a[i] - k) r[0][i] = mid, ll = mid + 1;
else rr = mid - 1;
} R[0][i] = r[0][i];
}
for (int i = 1; i <= 17; ++i)
for (int j = 1; j <= n; ++j) {
l[i][j] = l[i - 1][l[i - 1][j]];
r[i][j] = r[i - 1][r[i - 1][j]];
L[i][j] = L[i - 1][j] + L[i - 1][l[i - 1][j]];
R[i][j] = R[i - 1][j] + R[i - 1][r[i - 1][j]];
}
int q, ql, qr; read (q);
while (q--) {
read (ql), read (qr);
int res = qr - ql, now = ql;
for (int i = 17; i >= 0; --i)
if (l[i][now] <= qr) res -= L[i][now], now = l[i][now];
now = qr;
for (int i = 17; i >= 0; --i)
if (r[i][now] >= ql) res += R[i][now] + c[i], now = r[i][now];
printf ("%lld
", res + 1);
}
return 0;
}
E - Shuffle Window
一开始我想的是对每个数弄。。。发现逆序对这个东西真不好搞
跟“逆序对”关联的是每两个数的相对位置(前后关系),所以只要求出每两个数相对位置发生改变的概率
先把原本打乱序列的过程进行转换,发现等价于:
有一个集合(s)和一个初始为空的序列(a),初始时(s)有(1—k) (k)个元素, 进行(n-k)次操作,第(i)次从(s)中随机选择一个位置放到(a)的末尾(等同于打乱),然后删除这个元素,并插入(k+i)
有了这样的转化,就可以简单的算出对于(i<j),(i、j)相对位置改变的概率
首先,如果两个数要改变相对位置,那么必须存在一个时刻,他们同时在集合中。设(t[i])为第(i)个位置加入集合的时间
每次会随机从(k)个数中选出(1)个踢出集合,留下的概率是(frac{k-1}{k}),如果(i,j)要同时存在于集合中,则从(i)加入集合到(j)加入集合间的(j-i)次操作,(i)都不能被踢出集合,概率为({(frac{k-1}{k})}^{t[j]-t[i]})
如果两个数都在集合中,易得改变的概率为(0.5)
把上述式子的(i,j)分开,可以从(1—n)遍历(j),准便用树状数组求出(i)部分的贡献
怎么会有这么丑的代码...
#include <bits/stdc++.h>
using namespace std;
#define int long long
void read (int &x) {
char ch = getchar(); x = 0;
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
} const int N = 2e5 + 5, mod = 998244353;
int n, k, m, res, a[N], p[N], c[N], _c[N], __c[N], pw[N];
int qpow (int x, int y) {
int t = 1;
while (y) {
if (y & 1) t = t * x % mod;
x = x * x % mod, y >>= 1;
} return t;
}
#define plus(x, y) ((x += y) >= mod && (x -= mod))
void add (int x, int v) {
for (; x <= n; x += x & -x) plus (c[x], v);
}
int query (int x) {
int t = 0;
for (; x; x -= x & -x) plus (t, c[x]);
return t;
}
void _add (int x, int v) {
for (; x <= n; x += x & -x) plus (_c[x], v);
}
int _query (int x) {
int t = 0;
for (; x; x -= x & -x) plus (t, _c[x]);
return t;
}
void __add (int x) {
for (; x <= n; x += x & -x) ++__c[x];
}
int __query (int x) {
int t = 0;
for (; x; x -= x & -x) plus (t, __c[x]);
return t;
}
signed main() {
read (n), read (k);
for (int i = 1; i <= n; ++i) read (a[i]);
for (int i = 1; i <= k; ++i) p[i] = 0;
for (int i = k + 1; i <= n; ++i) p[i] = i - k;
m = (k - 1) * qpow (k, mod - 2) % mod; pw[0] = 1;
for (int i = 1; i <= n; ++i) pw[i] = pw[i - 1] * m % mod;
for (int i = 1; i <= n; ++i) {
int x = _query (a[i]), y = query (n - a[i] + 1);
res = (res + (x - y) * pw[p[i]] + __query (n - a[i] + 1)) % mod;
int tmp = qpow (2 * pw[p[i]], mod - 2);
_add (a[i], tmp), add (n - a[i] + 1, tmp), __add (n - a[i] + 1);
}
return printf ("%lld
", (res + mod) % mod), 0;
}
F - Center Rearranging
这里用中文讲的很清楚了,搬运一下
首先B由A操作而来,B一定有连续一段是没被动过的,将它们称为M;M左边都被被动过,且最后一次被用了一个push_front操作,称为L;M右边最后一次操作是push_back,称为R
(Lcdots LMcdots MRcdots R)构成一个B的来源序列,显然只有(O(n^2))种,先枚举它,之后这个序列就确定了
考虑B中的一种数(有3个)。A中也有3个这种数。从A,B中单独抽出这些数,考虑他们是怎么匹配的。
先考虑B中这3个数(位置从小到大)的来源,共有10种序列,其中(LLL,RRR)显然是不可能的,剩下8种可能情况。经过艰苦卓绝的手算可以发现,除了(LMR)之外,所有M的来源位置其实是确定的,如下表
第1组
M** ***
LMR LLR
第2组
**M ***
LMR LRR
---
M** **M
MRR LLM
MMM
MMM
M*M M*M
LMM MMR
确定M来源位置的用处是用于答案可行性的判定。对于所有(M),在A中原位置和B中新位置连一条边,显然这些边不能相交。(注意(M)不会被操作)
而(LMR)的M的来源位置也只有两种可能,现在暂且先确定每个(LMR)的M来源位置。
对于上面的表,先考虑一个简单的情况:如果(B)只有下面3组时怎么求解最小步数,可以发现,最小步数就是3n-M的数量,这显然也是下界。因为可以直接根据B中的位置来做所有L,R操作到达B的状态。
考虑加上1,2组之后怎么构造方案。1,2组有一个特点,就是不能乱做操作。比如第一组中的LMR,它只能是先做push_back再做push_front,也就是R的生成时间一定得比L更早。LLR也是,由于最右边的那个最后一次操作不可能是push_front,所以R的生成时间一定得比第一个L早。(当然了,第二个L的生成时间也得比第一个L早)第2组同理,L的生成时间必须比最后一个R早。
对于第1组,在B上第3个位置向第1个位置连一条边;对于第2组,在B上第1个位置向第3个位置连一条边,代表B中一个元素必须比另一个先生成。B上还存在一些边,就是右边的L元素向左边的L连边,左边的R向右边的R连边。这些所有边构成一个拓扑图,可以感性理解不存在其它限制(雾)。如果没有环就可以按照拓扑序做操作,完美还原序列。
怎么判环呢?可以发现,如果这个图存在环,一定可以抽出这样的环:一个第1组的第1,3位置都比一个第2组的1,3位置更小。这样的话判环就转化成了判一个二元关系。
还有一个限制“M连的边不相交”其实也是一个二元关系。
注意(LMR)的类型还没确定,不过只有两种情况可以用2sat求解。因为限制都是二元关系所以就很好做了。
#include <bits/stdc++.h>
using namespace std;
void read (int &x) {
char ch = getchar(); x = 0;
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
} const int N = 105;
int n, m, kind[N], dfn[N], bl[N], low[N], num, tp, st[N], in[N], tot;
#define pb push_back
vector<int> posa[N], posb[N], to[N]; vector<pair<int, int> > match;
#define fi first
#define se second
void tarjan (int u) {
dfn[u] = low[u] = ++num;
st[++tp] = u; in[u] = 1;
for (auto v : to[u])
if (!dfn[v]) tarjan (v), low[u] = min (low[u], low[v]);
else if (in[v]) low[u] = min (low[u], dfn[v]);
if (dfn[u] == low[u]) {
++tot; int t;
do t = st[tp--], bl[t] = tot, in[t] = 0; while (t != u);
}
}
int judge (int l, int r) {
match.clear(); memset (kind, 0, sizeof (kind));
#define get(p) (p <= l ? "L" : p >= r ? "R" : "M")
for (int i = 1; i <= n; ++i) {
string str = (string)get (posb[i][0]) + get (posb[i][1]) + get (posb[i][2]);
if (str == "LLL" || str == "RRR") return 0;
if (str == "LLR") kind[i] = 1; if (str == "LRR") kind[i] = 2; if (str == "LMR") kind[i] = 3;
if (str == "MMR") match.pb ({posa[i][0], posb[i][0]}), match.pb ({posa[i][2], posb[i][1]});
if (str == "LMM") match.pb ({posa[i][0], posb[i][1]}), match.pb ({posa[i][2], posb[i][2]});
if (str == "MMM") for (int j = 0; j < 3; ++j) match.pb ({posa[i][j], posb[i][j]});
if (str == "LLM") match.pb ({posa[i][2], posb[i][2]});
if (str == "MRR") match.pb ({posa[i][0], posb[i][0]});
}
sort (match.begin(), match.end());
#define check(a, b) (posb[a][0] > posb[b][0] && posb[a][2] > posb[b][2])
for (int i = 1; i < match.size(); ++i)
if (match[i].se < match[i - 1].se) return 0;
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
if (kind[i] == 1 && kind[j] == 2 && check (i, j)) return 0;
for (int i = 1; i <= n * 2; ++i) to[i].clear();
#define cross(a, b, c, d) ((a < c) ^ (b < d))
// if (l == 2 && r == 8) puts ("666");
for (int i = 1; i <= n; ++i) {
if (kind[i] != 3) continue; int tmp = 0;
for (auto j : match) { //根据 M 交叉限制条件
if (cross (j.fi, j.se, posa[i][0], posb[i][1])) tmp |= 2;
if (cross (j.fi, j.se, posa[i][2], posb[i][1])) tmp |= 1;
}
for (int j = 1; j <= n; ++j)
if (kind[j] == 1) { if (check (j, i)) tmp |= 1; }
else if (kind[j] == 2) { if (check (i, j)) tmp |= 2; }
else {
if (check (i, j) || cross (posa[i][0], posb[i][1], posa[j][2], posb[j][1])) to[i].pb (j);
if (check (j, i) || cross (posa[i][2], posb[i][1], posa[j][0], posb[j][1])) to[i + n].pb (j + n);
if (cross (posa[i][0], posb[i][1], posa[j][0], posb[j][1])) to[i].pb (j + n);
if (cross (posa[i][2], posb[i][1], posa[j][2], posb[j][1])) to[i + n].pb (j);
}
if (tmp == 3) return 0;
if (tmp & 1) to[i + n].push_back (i);
if (tmp & 2) to[i].push_back (i + n);
}
memset (dfn, 0, sizeof (dfn));
memset (bl, 0, sizeof (bl));
memset (in, 0, sizeof (in));
memset (low, 0, sizeof (low));
num = tp = tot = 0;
for (int i = 1; i <= n * 2; ++i) if (!dfn[i]) tarjan (i);
for (int i = 1; i <= n; ++i) if (bl[i] == bl[n + i]) return 0; return 1;
}
signed main() {
read (n), m = n * 3; int res = -1;
for (int i = 1, x; i <= m; ++i) read (x), posa[x].pb (i);
for (int i = 1, x; i <= m; ++i) read (x), posb[x].pb (i);
for (int l = 0; l <= m; ++l)
for (int r = l + 1; r <= m + 1; ++r)
if (judge (l, r)) res = max (res, r - l - 1);
return printf ("%d
", res > -1 ? m - res : -1), 0;
}