ACL Contest 1

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;
}
原文地址:https://www.cnblogs.com/whx666/p/13717748.html