对清北学堂寒假精英班内容的整理

Qbxt 寒假精英班 1.28

例题1 全排列

按照字典序从小到大枚举长度为N的全排列。

从后往前找到第一个非增的元素。

再从后往前找第一个比它大的元素。

交换两个元素。

反转后缀 。

时间复杂度\(_{max} = O(N)\)(一次操作)。 均摊\(O(N !)\)


例题2 激光炸弹

二位前缀和优化。

for (int i = 1 ; i <= M ; i ++)
	Sum[X[i]][Y[i]] = V[i] ;
for (int i = 1 ; i <= N ; i ++)
for (int j = 1 ; j <= N ; j ++)
	Sum[i][j] += Sum[i - 1][j] + Sum[i][j - 1] - Sum[i - 1][j - 1] ;

例题3 Mines for Diamonds

(UVa10606)

\(N \times M\)矩阵中有若干钻石,要求找到若干条道路使得从规定边界开始覆盖掉所有的钻石,最小化道路方格。

钻石数 \(≤ 10\)

  1. Bfs钻石间和钻石边界的曼哈顿距离。

  2. 枚举钻石排列。

对于每一个钻石,按照枚举的排列,可以选择向后连接下一个钻石,也可以选择成为链终点。贪心取min即可。

  1. 贪心扫一遍。

技巧

  1. 减少枚举状态。
  2. break优化:当得到答案时停止枚举。
  3. 卡时:在将要超时的时候停止枚举,直接输出当前最优解。(以一秒为例)
while ((double)clock() / CLOCKS_PER_SEC < 1) work() ;

迭代加深搜索


void IdDfs(int Now, int D) {
	if (D < 0) return ; Work(Now) ;
	for (Now, To) in Edges
		Dfs(Now, D - 1) ;
}
for (int D = 1 ; d <= Max_DEPTH ; D ++) IdDfs(Root, D) ;

适用于需要寻找最优解,但是空间不够\(Bfs\)的情况。

限定范围,然后寻找答案,如果没有答案,就扩大范围继续搜索。


例题4 骑士精神

  1. \(Iddfs\)
  2. 最优性剪枝:当前局面与目标局面相差棋子数 \(≥\)剩余步数$ + 1$时剪枝。

或者直接\(A^{*}\)


\(A^*\)算法

启发式算法。,用于寻找\(S -> T\)的最短路。设\(H[Now]\)为一个估价函数,\(G[Now]\)表示从起点\(S\)走到\(Now\)的最小代价。且定义\(F[Now] = G[Now] + G[Now]\)

\(A^*\)中,\(H[Now]\)必须小于等于\(Now\)到终点的真实最短路:正确性。

反之,若所有的\(H[Now]\)都小于等于\(Now\)\(T\)的真实最短路,那么我们一定可以找到最短路。

//Q 为一个小根堆。
G[S] = 0 ; F[S] = H[S] ; Q.push(S) ;
while (! Q.empty() && Q.front() != T){
	int Now = Q.front() ; Q.pop() ;
	Vis[Now] = true ;
	for (Now, To) in Edges
		if (! Vis[To] && G[Now] + Dis(Now, V) < G[To]) {
			G[To] = G[Now] + Dis(Now, V) ;
			F[To] = G[To] + H[To] ;
			Q.push(To) ;
		}

可以看到如果有\(H == 0\),那么上示代码就是个\(Dij\)

当然还有迭代加深版的\(IDA^*\):限制最大的\(F\)函数。

if (G + H[Now] > MaxF) return G + H[Now] ;

以及函数外的

int MaxF = H[S] ;
while (MaxF != FOUND) MaxF = IDA(S, 0, MaxF) ;

循环中:

T = IDA(To , G + Dis[Now, V], MaxF) ;
if (T == FOUND) return FOUND ;
CHKMIN(Min, T) ;

例题5 序列

(Bzoj 5449)

给定一个\(1 .. N\)的排列\(P\),每可以将\(P\)的某一个前缀反转,求出将序列变为升序的最小操作次数。

估价函数设计为当前相邻两个数字绝对值大于1的个数,因为一次翻转最多消除一个这样的位置,所以最终答案肯定大于这个值。然后迭代加深搜索,用估价剪枝就可以了。


分治

分治的本质是分支的本质缩小问题规模。

例如:定义点对\((X, Y)\)的权值为\(F(X, Y)\),要求统计\(\sum_{X }\sum_{Y} (F(X, Y) == S) ? 1 : 0\)

分治的过程一定只能和自己的区间的长度相关,基本可以化\(N\)\(logN\)这样子。


例题6 简单题

给定一个数列A,和两个参数\(QL, QR\),求出满足\(QL < a_xa_y < QR(X < Y)\)的点对个数。

对于分治区间\([L, R)\), 首先递归到\([L, Mid),[Mid, R)\),然后将两个区间内的数分别排序。

利用\(Two~Pointers\)\(i\)指向\(A_1\)\(j\)指向\(B_N\),且\(A, B\)均递增。然后每当\(i\)移动时,为了满足\(a_X + B_Y < C\),当\(i\)向右移动时,\(j\)只能向左移动。

B[0] = - Inf ;
for (int i = 1, j = N ; i <= N ; i ++){
	while (a[i] + B[i] >= Bound) j -- ;
	Total += j ;
}

加上排序,总复杂度是\(O(Nlog^2N)\)。将排序改为归并排序可以将时间复杂度优化到\(O(NlogN)\)


例题7 区间的价值

定义\((L, R)\)区间价值为区间最大值\(\times\)区间最小值。求最大区间价值。\(N≤10^5\)。数据随机。

最有区间问题我们仍然考虑分治,但是取中点显然很麻烦。

min, max都在左边

min, max都在右边

min在左,max在右

min在右,max在左

显然很不好更新。

由于数据随机,我们仍然可以考虑其他的点依然可以保证复杂度,比如我们考虑区间中最小的点最为分割点。

区间扩大时,max可能会扩大,min可能会缩小。当答案变小仅当最小值变小。

当区间分为了\((L, Pos\_Min),(Pos\_Min, R)\),其中\(Pos\_Max\)属于右区间。那么所有满足\(X < Pos\_Min \&\& Y > Pos\_Max\)的区间\((X, Y)\)权值都是一样的。

只需要更新长度为\(|Pos\_Max - Pos\_min|\)\(|R - Pos\_Max|\)。然后取后缀最大值即可。


例题8 非常规

定义一个数列时合法的,当且仅当这个数列的每一个子串(连续子序列)。都只存在一个只出现过一次的元素。判断给定序列是否合法。

暴力线段树......

当然我们这里考虑分治。预处理\(Prev\)\(Next\),维护上一个\(A[i]\)出现的位置和下一个。然后我们就能快速判断\(A[i]\)是不是在区间\((L, R)\)中只出现了一次。(检查\(Prev[i],Next[i]\)是不是属于\(L, R\)就可以了)然后\(Check(L, R)\),检查区间\((L,R)\)是否合法。

找到一个在此区间中只出现过一次的元素\(P\),然后递归\(Check(L, P)\)\(Check(P, R)​\)就可以了 。

对于区间\(L,R\),从\(L,R\)同时出发寻找合法的\(P\)点。那么区间就被分成了两块,复杂度就正确了。


可以看到分治实际上就是启发式合并的逆过程...


例题9 稻草人

给出N个点的坐标,定义一块矩阵合法,当且仅当其满足下列条件时:

  1. 矩阵时平行于坐标轴的长方形。
  2. 左下角和右上角各有一个点。
  3. 矩阵内部不包含点。

求出合法矩阵的个数。

将所有点按照纵坐标排序,然后按纵坐标分治。

通过分治求出上下两块的答案,只需考虑左下角在下半部分,右上角在上半部分的答案。

左下角\((X_1, Y_1)\),右上角\((X_2, Y_2)\)

对于上半部分,对于所有横坐标在\([X_1, X_2]\)内的点要求纵坐标\(≥Y_2\)

对于下半部分,对于所有横坐标在\([X_1, X_2]\)内的点要求纵坐标\(\leq Y_1\)

枚举右上角,维护两个单调栈,在单调栈上二分擦护照符合条件的左下角。


其他分治:

树分治,CDQ分治,整体二分......

总之\(Noip\)打死都不会考就是了,省选还是有可能的。


1.28测试

T1 逗猫

现在你有一个\(01\)矩阵,你可以进行若干次操作,每一次操作可以将矩阵中的某一个元素换掉。现在要求你进行若干次操作后,使得矩阵满足下面两个条件,要要满足哪一个条件已经给出。

  1. 每一个元素的相邻上下左右元素中\(1\)的个数为奇数。
  2. 每一个元素的相邻上下左右元素中\(1\)的个数为偶数。

要求最小化操作个数。

对于\(30\%\)\(subtask\),枚举每一个元素是否更换然后检查。复杂度\(O(2^{NM}NM)\)

对于\(N = 2\)的时候,按列考虑,发现对于某一个元素,更换不会影响前前一列和后后一列,就是说只会影响相邻两列的元素,当一列发生改变的时候,相邻两列改变,然后其它列改变...整个矩阵就被确定了,因此枚举第一列的元素怎么戴帽子,然后检查最后一列的需求是否满足即可。复杂度\(O(4NM)\)

满分做法:由\(N = 2\)情况可推出将矩阵转置。复杂度\(O(2^{min(N, M)}NM)\)

#include <bits/stdc++.h>
const int MAXN = 305;
int n, m, c;
int a[MAXN][MAXN];
int main() {
    scanf("%d%d%d", &n, &m, &c);
    for (int i = 0; i < n; ++i)
        for (int j = 0; j < m; ++j)
            scanf("%d", n > m ? &a[i][j] : &a[j][i]);
    if (n <= m)
        std::swap(n, m);

    int ans = INT_MAX;
    for (int s = 0; s < (1 << m); ++s) {
        int b[MAXN][MAXN];
        for (int j = 0; j < m; ++j)
            b[0][j] = s >> j & 1;
        for (int i = 1; i < n; ++i)
            for (int j = 0; j < m; ++j) {
                int left = j > 0 ? b[i - 1][j - 1] : 0;
                int up = i > 1 ? b[i - 2][j] : 0;
                int right = j < m - 1 ? b[i - 1][j + 1] : 0;
                b[i][j] = left ^ up ^ right ^ c;
            }
        bool valid = true;
        for (int j = 0; j < m; ++j) {
            int left = j > 0 ? b[n - 1][j - 1]: 0;
            int up = n > 1 ? b[n - 2][j] : 0;
            int right = j < m - 1 ? b[n - 1][j + 1] : 0;
            valid &= left ^ up ^ right ^ c ^ 1;
        }
        if (valid) {
            int cur = 0;
            for (int i = 0; i < n; ++i)
                for (int j = 0; j < m; ++j)
                    cur += a[i][j] ^ b[i][j];
            if (cur < ans)
                ans = cur;
        }
    }
    printf("%d\n", ans == INT_MAX ? -1 : ans);
}

T2 换猫

现在有一个序列,每次会有操作交换某两个元素,每次输出序列逆序对的奇偶性

对于\(20\%\)\(subtask\),可以直接\(O(N^2)\)枚举所有的数对。时间复杂度\(O(N^2q)\).

对于\(40 \%\)\(subtask\),k可以利用树状数组或者是归并排序求逆序对,时间复杂度\(O(NqlogN)\)

对于\(70 \%\)\(subtask\),观察发现每次交换\(X,Y\)的位置,那么逆序对的奇偶性一定改变,于是对原序列求逆序对的奇偶性,每次操作时,如果\(X ≠ Y\),那么将上一次的答案取反。

满分做法,由于我们在预处理逆序对的时候只需要知道它的奇偶性,所以可以有更快的方法。因为我们知道\(1, 2,3...N\)的逆序对个数是0,那么我们就只需要求出原始序列是由多少布从\(1,2,3...N\)变换过来的。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e7 + 5;
int n, q;
int a[MAXN];
int seed;
int A = 20000527;
int B = 20000909;
int C = 20171210;
int randint(int x) {
    seed = (1LL * seed * A + C) % B;
    return (seed % x) + 1;
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        scanf("%d%d%d", &n, &q, &seed);
        for (int i = 1; i <= n; ++i)
            a[i] = i;
        for (int i = 2; i <= n; ++i)
            swap(a[i], a[randint(i - 1)]);
        int c = 0;
        for (int i = 1; i <= n; ++i) {
            if (!a[i])
                continue;
            ++c;
            int j = a[i]; a[i] = 0;
            while (j) {
                int t = a[j];
                a[j] = 0;
                j = t;
            }
        }
        int cur = (n - c) & 1;
        unsigned ans = 0;
        for (int i = 1; i <= q; ++i) {
            int x = randint(n), y = randint(n);
            cur ^= (x != y);
            ans = ans * 3 + cur;
        }
        printf("%u\n", ans);
    }
}

T3 运猫

对于\(n = 3\) 的情况,只需要分类讨论三个节点的关系即可。复杂度\(O(1)\)

对于\(n = c\) 的情况,最终情况每个节点恰有一只猫。预处理出每个节点之间的运输代价后,我
们可以枚举每只猫分别从哪个节点运输到了哪个,再判断是否合法即可。
复杂度\(O(n^n + n^2)\)

接下来,在说明算法前,我们需要证明几个结论:

  1. 设把一只猫从\(u\) 运到\(v\) 的代价是\(dis(u, v)\) ,那么\(max(dis(u;w); dis(w; v)) ≥ dis(u; v).\)
    这个结论是显然的,因为所有从\(u\)\(v\) 的道路包含了所有从\(u\)\(w\) 再到\(v\) 的道路。
  2. 设一个点上猫多于平均值的为盈余点,猫少于平均值的为亏损点,那么猫的运输只会由盈余
    点运向亏损点。
    假设猫由一个盈余点\(u\) 运输到了盈余点\(v\)。由于\(v\) 节点上的猫最终只有平均值个,因此\(v\)
    必然还会向外输出猫,设其运输到了\(w\)。由上一条结论,直接从\(u\)\(w\) 运输猫必然不会比从\(u\)
    \(v\) 再运到\(w\) 劣,所以不需要把猫从\(u\) 运输到\(v\)
    同理,亏损点之间的运输也是无必要的。
    于是我们的问题变成了:一张二分图,左边向右边运输,不同的连边有不同的代价,要求每个
    左边的点运输完特定的猫的数量同时最小化代价。这个问题可以直接使用最小费用最大流解决。
以上是50分的做法。

Pic1

Pic2

Pic3

#include <bits/stdc++.h>

const int MAXN = 1e5 + 5, MAXM = 5e5 + 5;

struct Edge {
    int u, v;
    int w;
    bool operator< (const Edge &rhs) const {
        return w < rhs.w;
    }
};

int n, m;
int a[MAXN];
Edge e[MAXM];
int fa[MAXN];
int size[MAXN];
long long sum[MAXN];

int getFather(int u) {
    return fa[u] == u ? u : fa[u] = getFather(fa[u]);
}

int main() {
    freopen("transfer.in", "r", stdin);
    freopen("transfer.out", "w", stdout);

    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i)
        scanf("%d", &a[i]);
    for (int i = 1; i <= m; ++i)
        scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);

    int cnt = 0;
    long long k = 0, ans = 0;

    std::sort(e + 1, e + m + 1);
    for (int i = 1; i <= n; ++i) {
        fa[i] = i;
        size[i] = 1;
        k += sum[i] = a[i];
    }
    assert(k % n == 0);
    k /= n;
    for (int i = 1; i <= m; ++i) {
        int u = e[i].u, v = e[i].v, w = e[i].w;
        int fu = getFather(u), fv = getFather(v);
        if (fu == fv)
            continue;
        long long du = sum[fu] - k * size[fu];
        long long dv = sum[fv] - k * size[fv];
        if (du > 0 && dv < 0)
            ans += std::min(du, -dv) * w;
        else if (du < 0 && dv > 0)
            ans += std::min(-du, dv) * w;
        ++cnt;
        fa[fu] = fv;
        size[fv] += size[fu];
        sum[fv] += sum[fu];
    }
    assert(cnt == n - 1);

    printf("%lld\n", ans);
}


例题9 Final Bazarek

(Bzoj 3721)

给定N个数,多次询问选择K个数使得和为奇数的最大和。其中\(N \leq 1e6,询问个数 \leq 1e6\)

从大到小排序,选出前K个,然后从前K个数中选出最小的一个奇数换成后\(N - K\)个数中最大的偶数,反之亦然。

所以算法就是预处理出后\(i\)位的最大奇数或者偶数,前\(i\)位数最小的奇数和偶数,然后将两种方案比较一下就可以了。


例题10 sam-Toy Cars

(Bzoj 1528)

目前有\(N\)个玩具,都放在架子上,地板上不能存在超过\(K\)个玩具,不在地板上的就要到架子上拿,二地板满了要放回去,要求最小化操作次数。

贪心,每次选择下次玩的石间最靠后的玩具放回去。用大根堆维护一下玩具下一次玩的时间。


例题11 Bohater

(Bzoj3709)

现在有N个怪物,打败怪物需要消耗对应的D[i]的生命值,但怪物死掉之后会掉落血药,恢复A[i]的生命值。给定初始的生命值不能死亡,求打怪顺序。

把所有的怪分为两类:最终可以回血\((A_i > D_i)\),最终会扣血\((A_i \leq D_i)\)

考虑一下情况:总血量102,D[1] = 100,A[1] = 0, D[2] = 2, A[2] = 1。那么按照顺序结果显然是不一样的。那么可以知道应该按照\(A[i]\)从大到小排序。

对于可以回血的怪,按照\(D[i]\)从小到大排序。对于不可以回血的怪,按照\(A[i]\)从大到小排序,如果存在某一个怪打不了,则无解。


最大公约数

欧几里得算法:\(gcd(A, B) = gcd(B, A ~ mod~ B)\)

inline int Gcd(int X, int Y) {
	if (! Y) return X ;
	return Gcd(Y, X % Y) ; 
}

最小公倍数

\(lcm(X, Y) = \frac{X \times Y}{gcd(X, Y)}\)

推广\(lcm(X, Y, Z)\)可以得到\(lcm(S) = \prod_{T \in S} gcd(T)^{(-1)^{|T |+ 1}}\),其中\(S\)为元素集合,T为子集。

子集反演

\(max{S} = \sum_{T \in S} (-1)^{|T| + 1} min(T)\)

扩展欧几里得

给定\(A, B\),求一组\(X, Y\)满足\(AX + BY = gcd(A, B)\)

若已知\(X_0, Y_0\)满足\(BX_0 + (A ~mod~B)Y_0 = gcd(B, A ~mod~ B)\)

\(BX_0 + (A - \lfloor\frac{A}{B} \rfloor \times B)Y_0 = gcd(B, A ~mod~ B)\)

inline int Exgcd(int A, int B, int X, int Y) {
	if (! Y) {
		X = 1, Y = 0 ; return A ;
	}
	int D = Exgcd(B, A % B, Y, X) ;
	Y -= X * (A / B) ;
	return D ;
}

\(gcd\)\(lcm\)的性质

\(lcm(S) = \prod_{T \in S} gcd(T)^{(-1)^{|T |+ 1}}\)

\(gcd(Fib(a), Fob(b)) = Fib(gcd(a, b))\)

\(gcd(X^a - ,X_b - 1) = X^{gcd(a, b)} - 1\)


例题12 Bzoj4833

已知\(F(N) = 2F(N - 1) + F(N - 2)\),且\(F(0) = 0, F(1) = 1\)。定义\(G(N) = lcm(F(1), F(2)...F(N))\)。求\(\sum_{i = 1}^N G(i) * i ~(mod~p)\)的值,其中\(p\)是一个质数。

可以推出\(F\)函数满足性质2:\(gcd(F(a), F(b)) = F(gcd(a, b))\)\(G(N) = \prod_{T \in 2^N} F(gcd(i))^{-1^{|T| + 1}}\), \(F(N) = \prod_{(D|N)}H(D)\).利用莫比乌斯反演求出\(H()\)

得到\(G(N) = \prod_{T \in 2^N}(\prod_{D|gcd_{i \in T^{(i)}}}H(d))^{(-1)^{|T| + 1}}\)最后得到\(G(N) = \prod_{D = 1}^N H(D)\)


整除分块

考虑当\(i = 1...N\)时,\(\lfloor \frac{N}{i} \rfloor\)的取值情况。

for (int i = 1, Last ; i <= N ; i = Last + 1) {
	int A = N / i ;
	Last = N / A ;
	Ret += F(A) * (Sum(last) - Sum(i - 1)) ;
	//Sum(i)快速求前缀和。
}

例题12 Hdu 5780

\(\sum_{1 \leq a, b \leq N}gcd(X^a - 1, X^b - 1)\)\(X, N \leq 100000\), 300组数据。

根据性质3,式子可以转化为\(\sum_{1 \leq a,b \leq N}X^{gcd(a,b) } - 1\)

\(Ans = \sum_{K = 1}^N(X^K-1) \sum [gcd(a,b) =K]\)

且有\(gcd(a,b) = k\) ->\(gcd(\frac ak,\frac bk) = 1\) ->\(gcd(a',b') = 1\) -> \(fai\)

\(Ans = \sum_{K = 1}^N (X^k -1)(2 \sum_{i = 1}^{\lfloor \frac NK \rfloor} \varphi(i) - 1)\)

可用整除分块。


费马小定理

\(a^{p - 1} \equiv 1 (mod~p)\),其中\(p\)是一个质数,且\(a\)不是\(p\)的倍数。

欧拉定理

\(a^{\varphi(p)} \equiv 1 (mod~p)\),其中\(a,p\)互质。

可以看出,欧拉定理是费马小定理的推广。

而在一般情况下:\(a^m \equiv a^{min(m, (m ~mod ~\varphi(n))+\varphi(N))} (mod~p)\)

一下是利用费马小求乘法逆元。


inline int Fpm(int X, int P) {
	int Ans = 1 ; int M = P ;
	X %= M ; P -= 2 ;
	while (P) {
		if (P & 1) Ans = Ans * X % M ;
		A = A * A % M ; P >>= 1 ;	
	}	return Ans % M ;
}

例题13 经典题

\(2^{2^{2^{2^{2^{2^{2^{...}}}}}}} ~mod ~p\)的值。

指数\(2^{🌫}\)必定是\(≥ \varphi(p)\)

那么\(a^b≡a^{(b ~mod~ φ(p))+φ(p)}\)。然后直接递归就可以了。

因为最多递归\(logp\)次,所以时间复杂度就是\(O(logp)\)


中国的剩余定理

求一元模线性方程组\(X \equiv a_i (mod~P_i)\)的一个通解,\(P_i\)两两互质。

\(P' = \prod P_i\),和\(P'_i = \frac{P'}{P_i}\)

\(T_i = P'^{-1}_i~ mod P_i\)

构造通解:\(X \equiv \sum a_iT_iP'_i (mod~P')\)


筛法

埃拉托斯特尼筛法


bool Prime[MAXN] ;
inline void S() {
	Prime[1] = false ;
	for (int i = 2 ; i <= N ; i ++) Prime[i] = 1 ;
	for (int i = 2 ; i <= sqrt(N) ; i ++)
		if ()
		for (int j = i * 2 ; i <= N ; j += i)
			NotP[i * j] = false ;
}

时间复杂度\(O(NloglogN)\)

欧拉筛法


int P[MAXN] ; bool Prime[MAXN] ;
inline void S() {
	for(int i = 1 ; i <= N ; i ++) Prime[i] = 1 ;
	for (int i = 2 ; i <= N ; i ++) {
		if (Prime[i]) Prime[++ Tot] = i ;
		for (int j = 0 ; j <= Tot && i * P[j] < N ; j ++) {
			Prime[i * P[j]] = false ;
			if (! i % P[j]) break ;
		}
	}
}

时间复杂度\(O(N)\),线性筛法。(其实也不是完全线性,很接近而已......)。

posted @ 2019-01-29 11:22  Sue_Shallow  阅读(234)  评论(0编辑  收藏  举报
Live2D