231113 杂题选谈 // 我们
标题是暗杠的一首有点老的歌,我从小听到大。
忘了什么时候开始,要清晨才能入睡
也忘了什么叫做结尾,又有谁在乎呢
凌晨三点的窗前,播放着那段时光
有一个骄傲的少年,隐藏他的青春
——《认真地老去》
A. 卡门
https://www.luogu.com.cn/problem/P6370
http://222.180.160.110:1024/contest/4434/problem/1
我们惊讶地发现全场没多少人会做绿题。
就是说呢,我们把它「滚动到相邻行然后落下」的过程直接变成「往左下 / 右下滚」。那么球掉下去的过程呢,就可以理解为它是一直一次往下掉一格的,然后每一次掉落呢,会根据实际情况往左 / 往右 / 直着掉。
所以我们可以直接用 \(m\) 个长度为 \(n\) 的序列,记录从每 \(1\sim m\) 列扔下去的情况。然后我们很容易可以知道,如果任意两个序列发生了交汇,那么从交汇点开始至序列结束呢,这两个序列的内容都是完全一样的。因为之前序列长什么样子,完全不会对之后产生影响。所以一旦有任意一个状态是一样的,后面都会是一样的。
所以呢,一旦我们跟着计划好的路线走,却发现走不动了,那就说明从走不动的位置开始,到原定路线结束,这些位置都会被封掉。所以这个时候我们直接重新计算路线就好了。
对于这 \(m\) 列,就算每一列预定路线上的每个位置都被占了一次,也只会被更改 \(n\times m\) 次;总体时间复杂度只有 \(\mathcal O(nm^2)\)。然后又由于不可能跑满,其实是完全没有问题,甚至跑得出溜快的。
namespace XSC062 {
using namespace fastIO;
const int maxm = 35;
const int maxn = 3e4 + 5;
char a[maxn][maxm];
int n, m, q, u, x, y;
std::vector<int> p[maxm];
int main() {
#ifdef ONLINE_JUDGE
freopen("kamen.in", "r", stdin);
freopen("kamen.out", "w", stdout);
#endif
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%s", a[i] + 1);
for (int i = 1; i <= m; ++i) {
x = 1, y = i;
for (;;) {
p[i].push_back(y);
if (a[x + 1][y] == '.') ++x;
else {
if (a[x + 1][y] == 'X') break;
if (a[x][y - 1] == '.' &&
a[x + 1][y - 1] == '.')
++x, --y;
else if (a[x][y + 1] == '.' &&
a[x + 1][y + 1] == '.')
++x, ++y;
else break;
}
}
}
scanf("%d", &q);
while (q--) {
scanf("%d", &u);
while (a[p[u].size()]
[p[u].back()] != '.')
p[u].pop_back();
x = p[u].size(), y = p[u].back();
for (;;) {
p[u].push_back(y);
if (a[x + 1][y] == '.') ++x;
else {
if (a[x + 1][y] == 'X') break;
if (a[x][y - 1] == '.' &&
a[x + 1][y - 1] == '.')
++x, --y;
else if (a[x][y + 1] == '.' &&
a[x + 1][y + 1] == '.')
++x, ++y;
else break;
}
}
a[x][y] = 'O';
}
for (int i = 1; i <= n; ++i)
puts(a[i] + 1);
return 0;
}
} // namespace XSC062
B. 商人
http://222.180.160.110:1024/contest/4434/problem/2
首先忽略图中的环带来的问题,假设 \(f_u\) 表示从 \(u\) 点出发的答案,不难想到 DP 式 \(f_u = \min\{\max(f_v-p_{(u,v)}, r_{(u,v)})\}\)。
那么问题来了,图中是会有环的,不能简单地去更新 DP 值。我们首先考虑「根源」,每一个 \(f_u\) 一定都是由另一个 \(f_v\) 转移得到的,那么一定会存在一个已知的 \(f_u\),不由其他任何 \(f_v\) 得到。
考虑一个环中 \(r\) 最大的一条边 \((u,v)\)。假设图中只有这一个环,那么 \(f_u\) 的值一定为 \(r\),因为 \(p\) 只能为非负。这样「根源」就被我们找到了。接下来进一步考虑转移方法。
一个点 \(u\) 只能被其相连的点 \(v\) 更新,那么当且仅当所有 \(f_v\) 都是确定的,\(f_u\) 才能被确定,进而去更新 \(u\) 的前驱。这让我们想到了拓扑排序。如果我们将所有边反向,那么上面的过程几乎就是拓扑排序。
为了保证任意一个环上的「根源」都能最先被确定,我们按照按 \(r\) 从大到小遍历每一条边的方式处理问题。对于每一条边 \((u,v)\),我们都假设它是「根源」并用 \(r_{(u,v)}\) 更新 \(f_u\)。我们其实并不关心 \(u\) 是否是我们刚刚定义的形式上的「根源」,毕竟,只要 \(f_u\) 能被 \(r_{(u,v)}\) 更新,它就能算作广义上的,当前时刻的「根源」。
我们在一开始将出度为 \(0\)(反图上就是入度为 \(0\))的点全部加入队列,在每次加边的时候进行拓扑排序。每次一条边以任意形式被「使用」后,都要打标记删除并不能再访问,以保证转移的正确性和高效。如果一条边被「使用」,分两种情况:
-
更新「根源」:
这个时候这条边已经发挥了它的所有作用了,作为 当前时刻的「根源」,这条边的源点在未来至少不会再经由这一条边被更新。故可以删除。
-
拓扑排序中转移:
一个点被加入队列,说明它的 DP 值是确定的,那么就不用拿一个已经确定的值多次更新另一个 DP 值。故可以删除。
所以正确性是可以保证的。类拓扑排序的结构也保证了算法复杂度 \(\mathcal O(n+m)\)。
#define int long long
namespace XSC062 {
using namespace fastIO;
const int inf = 1e18;
const int maxn = 2e5 + 5;
const int maxm = 2e5 + 5;
struct _ {
int v, r, p, i;
_() {}
_(int v1, int r1, int p1, int i1) {
v = v1, r = r1, p = p1, i = i1;
}
};
struct __ {
int x, y, r, p;
bool operator< (const __ &q) const {
return r > q.r;
}
};
__ a[maxm];
bool del[maxn];
std::queue<int> q;
int n, m, x, y, r, p;
std::vector<_> g[maxn];
int f[maxn], deg[maxn];
int max(int x, int y) {
return x > y ? x : y;
}
int min(int x, int y) {
return x < y ? x : y;
}
void add(int x, int y, int r, int p, int i) {
g[x].emplace_back(y, r, p, i);
return;
}
int main() {
#ifdef ONLINE_JUDGE
freopen("merchant.in", "r", stdin);
freopen("merchant.out", "w", stdout);
#endif
read(n), read(m);
std::fill(f + 1, f + n + 1, inf);
for (int i = 1; i <= m; ++i) {
read(a[i].x), read(a[i].y);
read(a[i].r), read(a[i].p);
++deg[a[i].x];
}
std::sort(a + 1, a + m + 1);
for (int i = 1; i <= n; ++i)
if (!deg[i]) q.push(i);
for (int i = 1; i <= m; ++i) {
add(a[i].y, a[i].x,
a[i].r, a[i].p, i);
}
for (int i = 1; i <= m; ++i) {
while (!q.empty()) {
int t = q.front();
q.pop();
for (auto j : g[t]) {
if (del[j.i]) continue;
del[j.i] = 1;
if (f[t] != inf) {
f[j.v] = min(f[j.v],
max(f[t] - j.p, j.r));
}
if (!--deg[j.v]) q.push(j.v);
}
}
if (!del[i]) {
del[i] = 1, f[a[i].x] =
min(f[a[i].x], a[i].r);
if (!--deg[a[i].x])
q.push(a[i].x);
}
}
for (int i = 1; i <= n; ++i)
print(f[i] == inf ? -1 : f[i], ' ');
putchar('\n');
return 0;
}
} // namespace XSC062
#undef int
后面就是数数题。
A - Gerald and Giant Chess
https://vjudge.net/contest/594162#problem/A
这个呢,一道简单的数数题,但是因为我忘了 DP 容斥怎么打了所以自行思考了很久很久很久,还差最后一点点想出来的时候放弃了去抄题解 /cf
就是,这种「限制通行」的DP 容斥(名字是我现场起的)类型的数数题有一种通法,就是首先我们只计算非法方案然后减掉。
那么非法方案怎么算呢?令 \(f_i\) 表示一个非法点都不经过,到达 \(i\) 这个非法点的方案数,那么有 \(f_i=calc(s,i)-\sum f_j\times calc(j,i)\),其中 \(s\) 是起点,\(calc(j,i)\) 是从 \(j\to i\) 的方案数。这样就能不重不漏地枚举完所有情况了。
我们把 \((n,m)\) 也视作一个非法点然后代入 DP 即可得到答案。
#define int long long
namespace XSC062 {
using namespace fastIO;
const int lim = 3e5;
const int mod = 1e9 + 7;
const int maxk = 2e3 + 5;
const int maxn = 3e5 + 5;
struct _ {
int x, y;
bool operator< (const _ &q) const {
return x == q.x ? y < q.y : x < q.x;
}
};
_ a[maxk];
int n, m, k, res;
int f[maxk], fac[maxn];
int qkp(int x, int y) {
int res = 1;
while (y) {
if (y & 1) (res *= x) %= mod;
(x *= x) %= mod, y >>= 1;
}
return res;
}
int inv(int x) { return qkp(x, mod - 2); }
int A(int n, int m) {
assert(n >= 0 && n - m >= 0);
return fac[n] * inv(fac[n - m]) % mod;
}
int C(int n, int m) {
return A(n, m) * inv(fac[m]) % mod;
}
int calc(int i, int j) {
int x = a[j].x - a[i].x + 1,
y = a[j].y - a[i].y + 1;
return C(x + y - 2, y - 1);
}
int main() {
fac[0] = 1;
for (int i = 1; i <= lim; ++i)
fac[i] = fac[i - 1] * i % mod;
bool flag = 0;
read(n), read(m), read(k);
for (int i = 1; i <= k; ++i) {
read(a[i].x), read(a[i].y);
flag |= (a[i].x == 1 && a[i].y == 1);
flag |= (a[i].x == n && a[i].y == m);
}
a[0].x = a[0].y = 1;
if (flag) { print(0, '\n'); return 0; }
++k, a[k].x = n, a[k].y = m;
std::sort(a + 1, a + k + 1);
for (int i = 1; i <= k; ++i) {
f[i] = calc(0, i);
for (int j = 1; j < i; ++j) {
if (a[j].x > a[i].x ||
a[j].y > a[i].y) continue;
f[i] -= f[j] * calc(j, i) % mod;
f[i] %= mod;
}
}
f[k] = (f[k] % mod + mod) % mod;
print(f[k], '\n');
return 0;
}
} // namespace XSC062
#undef int
B - Danger of Mad Snakes
https://vjudge.net/contest/594162#problem/B
#define int long long
namespace XSC062 {
using namespace fastIO;
const int lim = 1e3;
const int inf = 1e5;
const int mod = 1e9 + 7;
const int maxk = 1e5 + 5;
const int maxm = 1e3 + 5;
const int maxn = 2e3 + 5;
int n, m, k, res;
int s[maxm][maxm];
int fac[maxk], inv[maxk];
int X[maxn], Y[maxn], B[maxn];
int max(int x, int y) {
return x > y ? x : y;
}
int min(int x, int y) {
return x < y ? x : y;
}
int qkp(int x, int y) {
int res = 1;
while (y) {
if (y & 1) (res *= x) %= mod;
(x *= x) %= mod, y >>= 1;
}
return res;
}
int getinv(int x) {
return qkp(x, mod - 2);
}
int A(int n, int m) {
return fac[n] * inv[n - m] % mod;
}
int C(int n, int m) {
if (n < 0 || m < 0 || n < m)
return 0;
return A(n, m) * inv[m] % mod;
}
int calc(int i, int j) {
int u = max(1,
max(X[i] - k, X[j] - k)),
d = min(lim,
min(X[i] + k, X[j] + k)),
l = max(1,
max(Y[i] - k, Y[j] - k)),
r = min(lim,
min(Y[i] + k, Y[j] + k));
if (u > d || l > r) return 0;
int res = s[d][r] - s[u - 1][r] -
s[d][l - 1] + s[u - 1][l - 1];
return res;
}
int main() {
fac[0] = inv[0] = 1;
for (int i = 1; i <= inf; ++i) {
fac[i] = fac[i - 1] * i % mod;
inv[i] = getinv(fac[i]);
}
read(n), read(m), read(k);
for (int i = 1; i <= n; ++i) {
read(X[i]), read(Y[i]), read(B[i]);
++s[X[i]][Y[i]];
}
for (int i = 1; i <= lim; ++i) {
for (int j = 1; j <= lim; ++j) {
s[i][j] += s[i - 1][j] +
s[i][j - 1] - s[i - 1][j - 1];
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
int tmp = B[i] * B[j] % mod;
int tmq = C(n, m) -
C(n - calc(i, i), m) -
C(n - calc(j, j), m) +
C(n - calc(i, i) - calc(j, j) +
calc(i, j), m);
tmq = tmq % mod;
tmp = tmp * tmq % mod;
res = (res + tmp) % mod;
}
}
res = (res % mod + mod) % mod;
print(res, '\n');
return 0;
}
} // namespace XSC062
#undef int
—— · EOF · ——
真的什么也不剩啦 😖