zoukankan      html  css  js  c++  java
  • AtCoder Grand Contest 013

    题目传送门:AtCoder Grand Contest 013

    A - Sorted Arrays

    从前往后贪心取尽量长的合法前缀。

    #include <cstdio>
    
    int N, Ans;
    
    int main() {
    	scanf("%d", &N);
    	int lst = 0, sgn = 0;
    	for (int i = 1; i <= N; ++i) {
    		int x;
    		scanf("%d", &x);
    		if (!lst) ++Ans;
    		else if (!sgn) sgn = x > lst ? 1 : x < lst ? -1 : 0;
    		else if (sgn == 1) {
    			if (x < lst) ++Ans, sgn = 0;
    		} else {
    			if (x > lst) ++Ans, sgn = 0;
    		}
    		lst = x;
    	}
    	printf("%d
    ", Ans);
    	return 0;
    }
    

    B - Hamiltonish Path

    考虑我们有一条经过 (k) 个点的路径 ({ u, a_2, a_3, ldots , a_{k - 1}, v }),且其中 (a_1 = u) 以及 (a_k = v)

    若此时 (v) 还未满足相邻的点都在路径中的限制,任取一个相邻的不在路径中的点 (w),将 (w) 变为 (v) 的下一个点,即 (a_{k + 1} = w)

    注意这样的过程一定会在有限步内结束,因为每次会多取一个点,而当取满所有 (N) 个点时,限制一定会被满足。

    此时 (v) 已经满足其限制,对于 (u) 来说同理,只不过是将新点变为 (u) 的前一个点。用双端队列容易在 (mathcal O (N + M)) 的复杂度内维护。

    #include <cstdio>
    #include <algorithm>
    #include <vector>
    
    const int MN = 100005;
    
    int N, M;
    std::vector<int> G[MN];
    int vis[MN], A[MN], B[MN], C, D;
    
    int main() {
    	scanf("%d%d", &N, &M);
    	for (int i = 1, x, y; i <= M; ++i) {
    		scanf("%d%d", &x, &y);
    		G[x].push_back(y);
    		G[y].push_back(x);
    	}
    	A[C = 1] = 1;
    	B[D = 1] = 1;
    	vis[1] = 1;
    	while (1) {
    		int u = A[C], w = 0;
    		for (int v : G[u]) if (!vis[v]) w = v;
    		if (!w) break;
    		vis[w] = 1, A[++C] = w;
    	}
    	while (1) {
    		int u = B[D], w = 0;
    		for (int v : G[u]) if (!vis[v]) w = v;
    		if (!w) break;
    		vis[w] = 1, B[++D] = w;
    	}
    	printf("%d
    ", C + D - 1);
    	for (int i = C; i >= 2; --i) printf("%d ", A[i]);
    	for (int i = 1; i <= D; ++i) printf("%d%c", B[i], " 
    "[i == D]);
    	return 0;
    }
    

    C - Ants on a Circle

    这是某一道经典题的变形,经典题为:

    • 在一根长度为 (L) 的棍子上,有 (N) 只蚂蚁,位置互不相同且朝向左或右,其中蚂蚁的行为与本题相同。

    • 求出 (T) 时刻后每只蚂蚁的位置(如果蚂蚁从一端掉下请求出它从哪一段掉下)。

    这道经典题的做法是:

    • 假设棍子的左右两端都是无限长的,这样暂时不需要考虑掉下的情况。

    • 在两只蚂蚁碰撞时,我们假设蚂蚁其实没有掉头,而是直接穿过彼此。

    • 这样我们可以在 (mathcal O (N)) 的复杂度内,直接算出 (T) 时刻后哪些位置还存在蚂蚁,将这些位置排序,花费 (mathcal O (N log N))

    • 注意到在任意时刻,无论是有发生碰撞还是没有发生碰撞,所有蚂蚁之间的相对位置都不会改变。

    • 所以初始时在最左侧的蚂蚁,在最终时刻的位置一定是最小的那个位置,以此类推。

    • 最后如果位置超出了原棍子的范围,那么蚂蚁就是掉下了棍子。

    在本题中也是类似的,我们假设蚂蚁是穿过了彼此,这样可以求出 (T) 时刻后哪些位置上还存在蚂蚁。

    但是蚂蚁之间的相对位置无法比较显然地确定,我们仅知道蚂蚁在环上的相对位置是与原来循环同构的。

    我们考虑:一开始第 (i) 只蚂蚁携带着一个显示着数值 (i) 的电子屏,两只蚂蚁碰撞时看作它们穿过了彼此,但电子屏上的数值交换。

    也就是说任意时刻蚂蚁携带的电子屏上的数值,就是实际上该蚂蚁的编号(我们假设每只蚂蚁编号从不改变,仅有显示屏变化)。

    我们考虑,碰撞时,左侧的蚂蚁携带的电子屏上的数值实际上是增加了 (1),除非数值为 (N),那样必然是与携带着数值 (1) 的蚂蚁碰撞。

    同理右侧的蚂蚁携带的数值减少了 (1),除非数值为 (1),那样会变成 (N)

    但是,如果在模 (N) 的剩余系下考虑,就仅仅是左侧的蚂蚁携带的数值增加 (1),右侧的蚂蚁携带的数值减少 (1) 而已。

    最终取所在剩余类中,在 ([1, N]) 范围内的元素作为真实值即可。

    此时也就是要求每只蚂蚁与反向的蚂蚁碰撞的次数,如果该蚂蚁是朝顺时针方向(正方向)的,其编号增加碰撞次数,否则减少。

    这可以通过在另一朝向的所有蚂蚁中计算绕过了几圈,并对剩余的不足一圈的蚂蚁执行二分查找得出。

    时间复杂度为 (mathcal O (N log N))

    #include <cstdio>
    #include <algorithm>
    
    typedef long long LL;
    const int MN = 100005;
    
    int N, L, T, X[MN], W[MN], Y[MN], Id[MN];
    int X1[MN], X2[MN], C1, C2;
    int Ans[MN];
    
    inline LL Q1(LL v) {
    	LL w = (v % L + L) % L, k = (v - w) / L;
    	return std::upper_bound(X1, X1 + C1, w) - X1 + k * C1;
    }
    inline LL Q2(LL v) {
    	LL w = (v % L + L) % L, k = (v - w) / L;
    	return std::upper_bound(X2, X2 + C2, w) - X2 + k * C2;
    }
    
    int main() {
    	scanf("%d%d%d", &N, &L, &T);
    	for (int i = 0; i < N; ++i) {
    		scanf("%d%d", &X[i], &W[i]);
    		if (W[i] == 1) X1[C1++] = X[i];
    		if (W[i] == 2) X2[C2++] = X[i];
    	}
    	for (int i = 0; i < N; ++i) {
    		Y[i] = X[i];
    		if (W[i] == 1) {
    			Y[i] = (Y[i] + T) % L;
    			Id[i] = (i + Q2(X[i] + 2ll * T) - Q2(X[i])) % N;
    		}
    		if (W[i] == 2) {
    			Y[i] = (Y[i] - T % L + L) % L;
    			Id[i] = ((i - Q1(X[i] - 1) + Q1(X[i] - 2ll * T - 1)) % N + N) % N;
    		}
    		Ans[Id[i]] = Y[i];
    	}
    	for (int i = 0; i < N; ++i) printf("%d
    ", Ans[i]);
    	return 0;
    }
    

    D - Piling Up

    我们假设初始时红砖的数量为 (x_0),则蓝砖的数量应为 (N - x_0)

    同时,我们记第 (i) 次操作后的红砖的数量为 (x_i)

    观察第 (i + 1) 次操作时,可能执行的四种情况:

    1. 操作 RR:先拿出一块红砖,然后塞入一红一蓝两砖,再拿出一块红砖
      此操作仅能在 (x_i > 0) 时被执行,并且 (x_{i + 1} = x_i - 1)(oldsymbol{x}) 减少 (oldsymbol{1}))。
    2. 操作 RB:先拿出一块红砖,然后塞入一红一蓝两砖,再拿出一块蓝砖
      此操作仅能在 (x_i > 0) 时被执行,并且 (x_{i + 1} = x_i)不变)。
    3. 操作 BR:先拿出一块蓝砖,然后塞入一红一蓝两砖,再拿出一块红砖
      此操作仅能在 (x_i < N) 时被执行,并且 (x_{i + 1} = x_i)不变)。
    4. 操作 BB:先拿出一块蓝砖,然后塞入一红一蓝两砖,再拿出一块蓝砖
      此操作仅能在 (x_i < N) 时被执行,并且 (x_{i + 1} = x_i + 1)(oldsymbol{x}) 增加 (oldsymbol{1}))。

    对于一种操作序列,根据上面的 (x_i) 关于 (x_0) 的表达式以及不等式,我们可以确定出 (x_0) 对应的区间(或为空集,此时序列不合法)。

    我们考虑直接进行 DP:令 (mathrm{f}[i][j]) 表示 (x_i = j) 时前 (i) 次操作的方案数,初始化 (mathrm{f}[0][ast] = 1),答案就是 (mathrm{f}[N][ast]) 之和,转移显然。

    错误!这样计算会使得一个合法的操作序列被统计多次,也就是上面的 (x_0) 可能的区间内至少包含两个整数的情况。

    我们不妨考虑必须在 (x_0) 可能到达的最小值处统计一个操作序列。这意味着 (x_0) 哪怕再减小 (1),操作序列都将变得不合法。

    体现在 DP 过程中,即是存在一个时刻 (j) 值曾经到达过 (0),或者为 (1) 时执行了 RB 操作。

    所以我们更换状态定义,令 (mathrm{f}[i][j]) 表示 (x_i = j) 时前 (i) 次操作的方案数,限制是 (j) 从未到达过最小值。

    同时定义 (mathrm{g}[i][j]) 表示类似含义,但限制是 (j) 到达过至少一次最小值。

    转移仍然显然,答案即为 (mathrm{g}[N][ast]) 之和。时间复杂度为 (mathcal O (N M))

    #include <cstdio>
    
    typedef long long LL;
    const int Mod = 1000000007;
    const int MN = 3005;
    
    int N, M;
    int f[2][MN], g[2][MN], Ans;
    
    int main() {
    	scanf("%d%d", &N, &M);
    	int o = 0;
    	for (int i = 1; i <= N; ++i) f[o][i] = 1;
    	g[o][0] = 1;
    	for (int i = 1; i <= M; ++i) {
    		o ^= 1;
    		for (int j = 0; j <= N; ++j) f[o][j] = g[o][j] = 0;
    		for (int j = 0; j <= N; ++j) {
    			if (j) {
    				f[o][j] = (f[o][j] + f[o ^ 1][j - 1]) % Mod;
    				g[o][j] = ((LL)g[o][j] + g[o ^ 1][j - 1] + g[o ^ 1][j]) % Mod;
    				if (j == 1) g[o][j] = (g[o][j] + f[o ^ 1][j]) % Mod;
    				else f[o][j] = (f[o][j] + f[o ^ 1][j]) % Mod;
    			}
    			if (j < N) {
    				g[o][j] = ((LL)g[o][j] + g[o ^ 1][j + 1] + g[o ^ 1][j]) % Mod;
    				if (!j) g[o][j] = (g[o][j] + f[o ^ 1][j + 1]) % Mod;
    				else f[o][j] = ((LL)f[o][j] + f[o ^ 1][j + 1] + f[o ^ 1][j]) % Mod;
    			}
    		}
    	}
    	for (int j = 0; j <= N; ++j) Ans = (Ans + g[o][j]) % Mod;
    	printf("%d
    ", Ans);
    	return 0;
    }
    

    E - Placing Squares

    寻求组合意义:

    • (N + 1) 个间隔(包含位置为 (0)(N) 的间隔)中放置若干个隔板。

    • 其中位置 (0)(N) 必须放置隔板,且有 (M) 个位置禁止放置隔板。

    • 对于 (N) 个格子,每个格子中可以放球,蓝球或者红球。

    • 特别地需要满足:在相邻两个隔板间的每个格子中,蓝球的数量恰好为 (1),红球的数量也恰好为 (1)

    不难验证,对于一种放置隔板的方案,放球的方案数恰好为 (displaystyle prod_{i = 1}^{k} {(a_i)}^2),其中 (k) 为正方形个数,(a_i) 为第 (i) 个正方形的边长。

    此时我们即是要统计上述放置隔板和球的方案数。

    我们可以令 (mathrm{f}[i][j]) 表示仅考虑了前 (i) 个格子和前 (i + 1) 个间隔(也就是考虑到第 (i) 个格子右侧的间隔)时,且当最后一个隔板的右边的球数恰好为 (j) 时的放置方案数。

    显然 (j) 的取值为 ([0, 2])。我们容易写出 (mathrm{f}[i][ast])(mathrm{f}[i - 1][ast]) 转移的式子,有两种转移,取决于第 (i) 个格子右侧是否禁止了放置隔板。

    注意到这两种转移关于 (mathrm{f}[i]) 看作向量后,都是线性变换,可以被表示为矩阵的形式。

    那么也就是有 (N)(oldsymbol{A}) 矩阵连乘,然而实际上其中有 (mathcal O (M)) 个被替换成了 (oldsymbol{B}) 矩阵,求一向量乘矩阵的结果。

    显然使用矩阵快速幂可以做到 (mathcal O (M log N)) 的时间复杂度。

    #include <cstdio>
    
    typedef long long LL;
    const int Mod = 1000000007;
    
    #define F(i) for (int i = 0; i < 3; ++i)
    struct Mat {
    	int a[3][3];
    	Mat() {}
    	Mat(int v) {
    		F(i) F(j) a[i][j] = (i == j) * v;
    	}
    	int * operator [] (int i) {
    		return a[i];
    	}
    	friend Mat operator * (Mat a, Mat b) {
    		Mat c(0);
    		F(i) F(k) F(j) c[i][j] = (c[i][j] + (LL)a[i][k] * b[k][j]) % Mod;
    		return c;
    	}
    };
    
    int N, M;
    Mat A, B, Ans(1);
    
    int main() {
    	A[0][0] = 2, A[0][1] = 1, A[0][2] = 1;
    	A[1][0] = 2, A[1][1] = 1, A[1][2] = 0;
    	A[2][0] = 1, A[2][1] = 1, A[2][2] = 1;
    	B[0][0] = 1, B[0][1] = 0, B[0][2] = 0;
    	B[1][0] = 2, B[1][1] = 1, B[1][2] = 0;
    	B[2][0] = 1, B[2][1] = 1, B[2][2] = 1;
    	scanf("%d%d", &N, &M);
    	int lst = 0;
    	for (int i = 1, x; i <= M; ++i) {
    		scanf("%d", &x);
    		int e = x - lst - 1;
    		Mat C = A;
    		for (; e; e >>= 1, C = C * C)
    			if (e & 1) Ans = C * Ans;
    		Ans = B * Ans;
    		lst = x;
    	}
    	int e = N - lst - 1;
    	Mat C = A;
    	for (; e; e >>= 1, C = C * C)
    		if (e & 1) Ans = C * Ans;
    	Ans = B * Ans;
    	printf("%d
    ", Ans[2][0]);
    	return 0;
    }
    

    F - Two Faced Cards

    (N) 加上 (1),此时有 (X) 牌堆中仅有 (N - 1) 张牌,而 (Y) 牌堆中有 (N) 张牌,而 (Z) 牌堆中仍有 (Q) 张牌。

    注意到我们只关心 (X, Z) 牌堆中的数值,相对于 (Y) 牌堆中的数值的相对大小关系。

    我们先把牌堆 (Y) 中的牌变为 ({1, 2, 3, ldots , N}),然后用二分查找将 (X, Z) 牌堆中的数值进行变换,不改变相对于 (Y) 的大小关系。

    此时 (X, Z) 牌堆中的数值的值域为 ([1, N + 1])

    接下来考虑这个小问题:

    • 给定两个序列 (a_1 sim a_n)(b_1 sim b_n),问能否将 (a, b) 任意进行排列,使得最后对于任意的 (i) 均有 (a_i le b_i) 被满足。

    显然我们可以对 (a, b) 从小到大排序然后依次检查。但是本题中我们采用这样一个特殊的方法:

    • 令数组 (mathrm{s}) 初始为全 (0)
    • 对于每个 (a_i),将 (mathrm{s}[a_i], mathrm{s}[a_i + 1], mathrm{s}[a_i + 2], ldots) 全部加上 (1),即执行一个后缀加。
    • 对于每个 (b_i),将 (mathrm{s}[b_i], mathrm{s}[b_i + 1], mathrm{s}[b_i + 2], ldots) 全部减去 (1),即执行一个后缀减。
    • 最终 (mathrm{s}) 数组中的每个元素必须都大于等于 (0)

    不难验证这个方法的正确性。回到原问题:

    因为要最大化取正面的牌数,我们假设 (X) 牌堆中的所有牌初始时都是取正面的,也就是取了 (A_i)

    然后 (Y) 牌堆中的牌依次为 (C_i)。所以可以用一次前缀和计算出此时的 (mathrm{s}) 数组(注意值域只有 (N),所以 (mathrm{s}) 的有意义下标为 ([1, N]))。

    然而此时 (mathrm{s}) 数组一定不满足全部元素 (ge 0) 的性质。我们有若干补救措施:

    1. 对于 (X) 牌堆中的所有牌,如果 (B_i < A_i),我们可以花费 (1) 代价把它翻成反面,令 (mathrm{s}) 中下标在 ([B_i, A_i)) 中的元素加 (1)
    2. 对于 (Z) 牌堆中的一张牌(此时是处理 (Q) 个询问),选择 (x = D_i ext{ or } E_i),令 (mathrm{s}) 中下标在 ([x, N]) 中的元素加 (1)

    对于 (x = D_i ext{ or } E_i),可以两种情况都询问一次,然后合并,所以变成仅仅是给定一个 (x),下标 ([x, N])(1)

    我们把 ([x, N])(1) 的操作撤销掉,现在变成仅使用 ([B_i, A_i))(1) 的操作,使得:

    • 对于 (i < x)(mathrm{s}[i] ge 0);且对于 (i ge x)(mathrm{s}[i] ge -1)

    小结一下,此时有一个有正有负的数组 (mathrm{s}[1 sim N]),有若干区间 ([B_i, A_i)),和一个不确定的数 (x)
    我们需要选若干区间把 (mathrm{s}) 对应区间内元素加上 (1) 然后使得 (mathrm{s}[i] ge -[i ge x]),选择的区间个数越少越好。

    从后往前考虑 (mathrm{s}) 中的元素,当遇到第一个 (le -2) 的元素时,考察包含它的所有区间,对于左端点最左的区间,选取它是不劣的。

    这样一直下去直到 (mathrm{s}) 中的所有元素都 (ge -1) 为止,此时还有一些区间没有被使用。

    注意如果在这个过程中,有一个需要被区间覆盖的点上的区间已经用光了,则所有答案均为 (-1)

    然后考虑对所有可能的 (x) 都计算答案。例如如果前 (p)(mathrm{s}) 中的元素都大于等于 (0),则 (mathrm{Ans}[1 sim (p + 1)]) 为上一步选取的区间数。

    从前往后考虑 (mathrm{s}) 中的元素,当遇到第一个等于 (-1) 的元素时,考察包含它的所有区间,对于右端点最右的区间,选取它是不劣的。

    每选取一个区间就可以更新一些 (x) 的答案了。

    注意如果在这个过程中,有一个需要被区间覆盖的点上的区间已经用光了,则目前还没更新的 (x) 的答案就为 (-1)

    求出每个 (x) 对应的最少选取的区间数后,对于每个询问即可枚举是选择 (D_i) 还是 (E_i) 然后计算答案了。

    当需要求出一个需要被区间覆盖的点上的左端点最左(右同理)的区间时,可以使用优先队列维护(std::priority_queue)。

    时间复杂度为 (mathcal O ((N + Q) log N))

    #include <cstdio>
    #include <algorithm>
    #include <queue>
    
    const int MN = 100005, MQ = 100005;
    
    int N, Q, nA[MN], nB[MN], nC[MN], qD[MQ], qE[MQ];
    inline void tr(int &x) { x = std::lower_bound(nC + 1, nC + N + 1, x) - nC; }
    
    int A[MN], B[MN], used[MN];
    std::vector<int> R[MN];
    std::priority_queue<std::pair<int, int>> pq;
    int Ans[MN];
    
    int main() {
    	scanf("%d", &N), ++N;
    	for (int i = 1; i < N; ++i) scanf("%d%d", &nA[i], &nB[i]);
    	for (int i = 1; i <= N; ++i) scanf("%d", &nC[i]);
    	scanf("%d", &Q);
    	for (int i = 1; i <= Q; ++i) scanf("%d%d", &qD[i], &qE[i]);
    	std::sort(nC + 1, nC + N + 1);
    	for (int i = 1; i < N; ++i) tr(nA[i]), tr(nB[i]);
    	for (int i = 1; i <= Q; ++i) tr(qD[i]), tr(qE[i]);
    	for (int i = 1; i < N; ++i) ++A[nA[i]];
    	for (int i = 1; i <= N; ++i) A[i] += A[i - 1] - 1;
    	for (int i = 1; i < N; ++i)
    		if (nA[i] > nB[i]) R[nA[i] - 1].push_back(i);
    		else used[i] = 1;
    	for (int i = N, S = 0; i >= 1; --i) {
    		for (int j : R[i]) pq.push({-nB[j], j});
    		S += B[i];
    		while (A[i] + S < -1) {
    			while (!pq.empty() && -pq.top().first > i) pq.pop();
    			if (pq.empty()) {
    				while (Q--) puts("-1");
    				return 0;
    			}
    			auto p = pq.top(); pq.pop();
    			int j = p.second, l = nB[j], r = nA[j] - 1;
    			++S, ++B[r], --B[l - 1], used[j] = 1, ++Ans[1];
    		}
    	}
    	for (int i = N; i >= 1; --i) A[i] += B[i] += B[i + 1];
    	for (int i = 1; i <= N; ++i) B[i] = 0, R[i].clear();
    	for (int i = 1; i < N; ++i) if (!used[i]) R[nB[i]].push_back(i);
    	for (int i = 1, S = 0; i <= N; ++i) {
    		Ans[i + 1] = Ans[i];
    		for (int j : R[i]) pq.push({nA[j] - 1, j});
    		S += B[i];
    		while (A[i] + S == -1) {
    			while (!pq.empty() && pq.top().first < i) pq.pop();
    			if (pq.empty()) {
    				for (int j = i; j <= N; ++j) Ans[j + 1] = N + 1;
    				goto where;
    			}
    			auto p = pq.top(); pq.pop();
    			int j = p.second, l = nB[j], r = nA[j] - 1;
    			++S, ++B[l], --B[r + 1], ++Ans[i + 1];
    		}
    	}
    	where:
    	for (int i = 1; i <= Q; ++i) {
    		int ans = -1;
    		ans = std::max(ans, N - Ans[qD[i]]);
    		ans = std::max(ans, N - Ans[qE[i]] - 1);
    		printf("%d
    ", ans);
    	}
    	return 0;
    }
    
  • 相关阅读:
    深度学习学习总结
    线性回归学习总结
    机器学习常用算法总结
    2020年,给自己立个flag
    与博客添网易云外钟墯之坑
    给博客添加网易云外链时掉的坑
    BEGINNING
    【华为云技术分享】程序员真香定律:源码即设计
    【华为云技术分享】【DevCloud · 敏捷智库】如何利用核心概念解决估算常见问题
    差点被祭天!狂欢618,且看研发人如何绝地求生
  • 原文地址:https://www.cnblogs.com/PinkRabbit/p/AGC013.html
Copyright © 2011-2022 走看看