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\))的点全部加入队列,在每次加边的时候进行拓扑排序。每次一条边以任意形式被「使用」后,都要打标记删除并不能再访问,以保证转移的正确性和高效。如果一条边被「使用」,分两种情况:

  1. 更新「根源」:

    这个时候这条边已经发挥了它的所有作用了,作为 当前时刻的「根源」,这条边的源点在未来至少不会再经由这一条边被更新。故可以删除。

  2. 拓扑排序中转移:

    一个点被加入队列,说明它的 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
posted @ 2023-11-15 14:41  XSC062  阅读(45)  评论(2编辑  收藏  举报