题目归档 #2
目录
- [APIO2015] 雅加达的摩天楼
- [SCOI2005] 互不侵犯
- CF280C Game on Tree
[APIO2015] 雅加达的摩天楼
一道算法为最短路的题目,但其实真正的考点是巧妙的建图方式,并且包含类似分块的思想。
题意
有 \(N\) 座摩天楼,和 \(M\) 只在摩天楼上的「doge」。第 \(i\) 只「doge」初始在第 \(b_i\) 座摩天楼,它的跳跃能力为 \(P_i\)。在一次跳跃中,当前位于摩天楼 \(x\) 而跳跃能力为 \(p\) 的「doge」可以跳跃到编号为 \(x-p\) 或 \(x+p\) 的在范围内的摩天楼。
编号为 \(0\) 的 「doge」 有一条紧急的消息要尽快传送给编号为 \(1\) 的 「doge」。任何一个收到消息的「doge」有以下两个选择:
- 跳跃到其他摩天楼上;
- 将消息传递给它当前所在的摩天楼上的其他 doge。
请计算将消息从 \(0\) 号「doge」传递到 \(1\) 号「doge」所需要的最少总跳跃步数,或者指出不存在答案。
- \(N \leq 30000,p_i \leq 30000,M \leq 30000\)
解题
第一眼很可能会想到最短路。没错,这道题的解决方法就是最短路。但是怎么建图是关键,我们可以先从暴力开始建图,而后一步一步地优化。
- 初步建图
首先,一只「doge」最开始在 \(b\),那么它可以去到的所有点就是 \(b+kp\),其中 \(k\) 为整数。那显然最暴力的一种建图方式就是把 \(b\) 和 \(b+kp\) 全连有向边,然后权值为 \(k\)(因为要跳 \(k\) 次)。显然这样建图可能会出现 \(NM\) 条边,显然不可接受。
- 尝试优化
注意到,很多边都是冗余的。比如有很多只「doge」的 \(p_i\) 均为 \(1\),那么其实建出的很多边可以缩减为更少的,效果一致的边集。我们考虑如下的建图方式:对于给定的 \(b,p\),在 \(...(b-p,b),(b,b+p),(b+p,b+2p),(b+2p,b+3p)...\) 之间分别连上长度为 \(p\) 的有向边。显然效果与上面初步建图的效果相同。但是这只能在 \(p\) 相同时使用,并且中间很多途径的节点(比如上面的 \(b-p,b+p,...\))也会被连上,因此程序会认为「doge」可以从该节点出发,实际上并不能。
于是,「分层建图」应运而生。对于不同的 \(p\) 建立不同的层,每层之间如以上方式连边。为了应对层与层之间的转换,我们需要一个「基础层」,也就是第 \(0\) 层。这一层的图将起到一个「中转枢纽」的作用,如果有一个「doge」初始在 \(b\),跳跃为 \(p\),那么我们就在基础层的点 \(b\) 向第 \(p\) 层的点 \(b\) 连边。同时注意,「doge」随时可以从上面的层下到「基础层」(「doge」可以在任意摩天楼停留),所以对任意摩天楼,从 \(1-p_{max}\) 层向第 \(0\) 层的对应节点都要连边。
这样就规避了建边冗余的问题,最坏情况下仍会建立 \(NM\) 级别的边,因为最多可能有 \(M\) 层。能否减少层数呢?
- 进一步优化
前面提到分块的思想。这个时候,我们考虑只建立 \(\sqrt{N}\) 层边。也就是说,对于 \(p_i \leq \sqrt{N}\) 的「doge」,按「尝试优化」中的方法建边,这样最多会产生 \(N\sqrt{N}\) 条。对于 \(p_i > \sqrt{N}\),直接按照「初步建图」在「基础层」中暴力建边。由于这些「doge」的跳跃能力大于 \(\sqrt{N}\),因此每只「doge」最多只会产生 \(\sqrt{N}\) 条边,这样最多会产生 \(M\sqrt{N}\) 条。显然最后的边数就被限制在了乘以根号的级别,可以接受。
最后用 SPFA 跑最短路即可(听说这题用 Dij 会 TLE),详细见代码部分。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#include <queue>
#define Maxn 6000010
#define Maxm 15000000
#define key 10007
#define rg register
#define LL long long
using namespace std;
const LL MOD = 1ll * 1000007;
inline int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, M, S, T, A, vis[MOD + 10];
struct Edge {
int next, to, dis;
}
edge[Maxm];
int head[Maxn], edge_num;
inline void add_edge(int from, int to, int dis) {
edge[++edge_num].next = head[from];
edge[edge_num].to = to;
edge[edge_num].dis = dis;
head[from] = edge_num;
}
queue <int> Q;
int dist[Maxn]; bool inq[Maxn], flag;
void SPFA() {
Q.push(S);
memset(dist, 63, sizeof(dist));
dist[S] = 0; inq[S] = 1;
while(Q.size()) {
int u = Q.front();//cout << u << endl;
if(u == T) flag = 1;
Q.pop(); inq[u] = 0;
for(int i = head[u]; i != -1; i = edge[i].next) {
int v = edge[i].to;
if(dist[u] + edge[i].dis < dist[v]) {
dist[v] = dist[u] + edge[i].dis;
if(!inq[v]) Q.push(v), inq[v] = 1;
}
}
}
}
int main() {
N = read();
M = read();
A = 100;
int b, p, now, maxA = 0;
memset(head, -1, sizeof(head));
for(rg int i = 0; i < M; ++i) {
b = read(); p = read();
if(i == 0) S = b;
if(i == 1) T = b;
if(p <= A) {
maxA = max(maxA, p);
add_edge(b, b + p * N, 0);
}
else {
now = b; while(now < N) add_edge(b, now, (now - b) / p), now += p;
now = b; while(now >= 0) add_edge(b, now, (b - now) / p), now -= p;
}
}
for(rg int i = 0; i < N; ++i)
for(rg int k = 1; k <= maxA; ++k)
add_edge(i + k * N, i, 0);
for(rg int k = 1; k <= maxA; ++k) {
for(rg int i = 0; i < N - k; ++i) add_edge(i + k * N, i + k * N + k, 1);
for(rg int i = N - 1; i >= k; --i) add_edge(i + k * N, i + k * N - k, 1);
}
SPFA();
if(flag) cout << dist[T] << endl;
else cout << "-1" << endl;
return 0;
}
[SCOI2005] 互不侵犯
一道比较套路的状压 dp。
题意
-
在 \(N \times N\) 的棋盘里面放 \(K\) 个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 \(8\) 个格子。
-
\(N \leq 9, K \leq N^2\)
解题
显然枚举列 \(i\) 为第一维(行也可以,道理是一样的),第二维二进制 \(j\) 表示这一列的情况,此外还需要两个辅助维度——第三层 \(k\) 表示上一列的情况,第四位 \(tot\) 表示一共摆放了多少个国王。
令 \(f[i][j][tot]\) 表示扫到第 \(i\) 列,当前状态为 \(j\),加上此列有 \(tot\) 个国王的总方案数。
状态转移方程显然:\(f[i][j][tot] += f[i - 1][k][tot - numj]\),\(numj\) 是这一列的国王数,可根据 \(j\) 算出。
最后预处理出第一列的方案数,即所有的 \(f[1][j][numj]\),dp 即可。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#define Maxn 9
#define Maxk 81
#define LL long long
using namespace std;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, K;
LL f[Maxn + 1][(1 << Maxn)][Maxk + 1];
int main() {
N = read(); K = read();
for(int j = 0; j < (1 << N); ++j) {
int cpj = j, numj = 0, flag = 0;
while(cpj) {
if((cpj & 1) && flag) {flag = 2; break;}
if(cpj & 1) flag = 1, ++numj;
else flag = 0;
cpj >>= 1;
}
if(flag != 2) f[1][j][numj] = 1;
}
for(int i = 2; i <= N; ++i) {
for(int j = 0; j < (1 << N); ++j) {
int cpj = j, numj = 0, flagj = 0;
while(cpj) {
if((cpj & 1) && flagj) {flagj = 2; break;}
if(cpj & 1) flagj = 1, ++numj;
else flagj = 0;
cpj >>= 1;
}
if(flagj == 2) continue;
for(int k = 0; k < (1 << N); ++k) {
int cpk = k, flagk = 0;
while(cpk) {
if((cpk & 1) && flagk) {flagk = 2; break;}
if(cpk & 1) flagk = 1;
else flagk = 0;
cpk >>= 1;
}
if(flagk == 2) continue;
if(j & k || (j >> 1) & k || (j << 1) & k) continue;
for(int tot = numj; tot <= K; ++tot) f[i][j][tot] += f[i - 1][k][tot - numj];
}
}
}
LL ans = 0;
for(int j = 0; j < (1 << N); ++j) ans += f[N][j][K];
cout << ans << endl;
return 0;
}
CF280C Game on Tree
一道代码简短,思想难度适中的概率期望题目。要用到贡献的思想。
题意
- 给出一棵 \(n\) 个点的树,每次随机等概率选择一未染黑的点,将它及其子树染黑。问期望多少次操作可以将树全部染黑。
- \(n \leq 10^5\)。
解题
显然从贡献的思想来考虑。
我们发现,对于某个点 \(u\),计其深度为 \(depth_u\)。显然,这个点 由他自己染黑 的概率为 \(\frac{1}{depth_u}\),由别人染黑的概率为 \(\frac{depth_u-1}{depth_u}\)。前一种这个点会对答案产生 \(1\) 的贡献,后一种贡献则为 \(0\)(因为这个点没有做任何事情就黑了,所以无贡献,实际上贡献是由它的某个祖先节点做出的)。
综上,一个点的贡献期望为 \(\frac{1}{depth_u}\),答案即为所有点的深度倒数之和。
程序
#include <iostream>
#include <cstring>
#include <cstdio>
#define Maxn 100010
using namespace std;
int read() {
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') {
if(c == '-') f = -1;
c = getchar();
}
while('0' <= c && c <= '9') {
x = x * 10 + c - '0';
c = getchar();
}
return x * f;
}
int N, depth[Maxn];
struct Edge {
int next, to;
}
edge[Maxn * 2];
int head[Maxn], edge_num;
void add_edge(int from, int to) {
edge[++edge_num].next = head[from];
edge[edge_num].to = to;
head[from] = edge_num;
}
void dfs(int u, int f) {
depth[u] = depth[f] + 1;
for(int i = head[u]; i; i = edge[i].next) {
int v = edge[i].to;
if(v == f) continue;
dfs(v, u);
}
}
int main() {
N = read();
int u, v;
for(int i = 1; i < N; ++i) {
u = read(); v = read();
add_edge(u, v);
add_edge(v, u);
}
dfs(1, 0);
double ans = 0;
for(int i = 1; i <= N; ++i) {
ans += (1.0 / depth[i]);
}
printf("%.9lf", ans);
return 0;
}

浙公网安备 33010602011771号