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\n", 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\n", 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], " \n"[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\n", Ans[i]);
return 0;
}
D - Piling Up
我们假设初始时红砖的数量为 \(x_0\),则蓝砖的数量应为 \(N - x_0\)。
同时,我们记第 \(i\) 次操作后的红砖的数量为 \(x_i\)。
观察第 \(i + 1\) 次操作时,可能执行的四种情况:
- 操作
RR
:先拿出一块红砖,然后塞入一红一蓝两砖,再拿出一块红砖。
此操作仅能在 \(x_i > 0\) 时被执行,并且 \(x_{i + 1} = x_i - 1\)(\(\boldsymbol{x}\) 减少 \(\boldsymbol{1}\))。 - 操作
RB
:先拿出一块红砖,然后塞入一红一蓝两砖,再拿出一块蓝砖。
此操作仅能在 \(x_i > 0\) 时被执行,并且 \(x_{i + 1} = x_i\)(不变)。 - 操作
BR
:先拿出一块蓝砖,然后塞入一红一蓝两砖,再拿出一块红砖。
此操作仅能在 \(x_i < N\) 时被执行,并且 \(x_{i + 1} = x_i\)(不变)。 - 操作
BB
:先拿出一块蓝砖,然后塞入一红一蓝两砖,再拿出一块蓝砖。
此操作仅能在 \(x_i < N\) 时被执行,并且 \(x_{i + 1} = x_i + 1\)(\(\boldsymbol{x}\) 增加 \(\boldsymbol{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\n", 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\) 个 \(\boldsymbol{A}\) 矩阵连乘,然而实际上其中有 \(\mathcal O (M)\) 个被替换成了 \(\boldsymbol{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\n", 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\) 的性质。我们有若干补救措施:
- 对于 \(X\) 牌堆中的所有牌,如果 \(B_i < A_i\),我们可以花费 \(1\) 代价把它翻成反面,令 \(\mathrm{s}\) 中下标在 \([B_i, A_i)\) 中的元素加 \(1\)。
- 对于 \(Z\) 牌堆中的一张牌(此时是处理 \(Q\) 个询问),选择 \(x = D_i \text{ or } E_i\),令 \(\mathrm{s}\) 中下标在 \([x, N]\) 中的元素加 \(1\)。
对于 \(x = D_i \text{ 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\n", ans);
}
return 0;
}