P8914 [DMOI-R2] 梦境
难点
- 如何得到字典序最小的最短路径。
- 如何找到最坏情况。
- 如何正确输出答案。
解决
第一个难点
我们可以以 F F F 为起点,以 S S S 为终点跑堆优化的 dijkstra,用 p a t h [ v ] path[v] path[v] 表示 v v v 的最小前驱(前驱即 v v v 从哪一个点转移过来),并在 dijkstra 的过程中更新,更新方式如下:
// tp == 0 表示的是在进行找路径的dijkstra,不用在意。
if (dis[tp][v] > dis[tp][u] + w)
{
dis[tp][v] = dis[tp][u] + w;
q.push ((node) {v, dis[tp][v]});
if (tp == 0) path[v] = u; // 最短路得到更新,path[v]也要更新。
}
else if (tp == 0 && dis[tp][v] == dis[tp][u] + w)
path[v] = min (path[v], u); // 如果u更小,则更新。
正确性:因为是“倒着跑”dijkstra,故
S
S
S 的最小前驱,实际上就是,小 A 从
S
S
S 出发踏上的使最短路径字典序最小的第一个点,依次类推得到的便是最小字典序的最短路径。
下面通过举反例的方式证明“正着跑”是错误的,看下图:
设
S
S
S 为
1
1
1,
F
F
F 为
5
5
5,如果“正着跑”按同样的方法记录前驱数组,得到的逃离路线是:
1
→
4
→
2
→
5
1\rightarrow4\rightarrow2\rightarrow5
1→4→2→5;而正确答案是:
1
→
3
→
5
1\rightarrow3\rightarrow5
1→3→5。故“正着跑”错误。
第二个难点
解决办法:
我们先从最简单的情况入手,即怪兽在小 A 的逃离路线上,若小 A 比怪兽离终点远,则是相遇问题;若小 A 比怪兽离终点近,则是追击问题。
然后我们复杂化情况,即怪兽不在小 A 的逃离路线上,则怪兽肯定需要走到小 A 的逃离路线上的某一个点,那么为了更可能地追上小 A,怪兽肯定走最短路径过去(dijkstra 又来啦!),走到小 A 的逃离路线某一个点后,就转化成较简单的情况了。但这里有两个问题:一个是怪兽到不了某个点,另一个是怪兽还没到逃离路线上小 A 就逃离了,需要特判一下。
第三个难点
输出问题,因为题目说了小数位不会超过两位,所以我们可以暴力判断,如下:
long long x = ans * 100;
if (x % 100 == 0) printf("%lld", x / 100);
else if (x % 10 == 0) printf("%.1lf", 0.01 * x);
else printf("%.2lf", 0.01 * x);
于是我们就美美地叫上去了,但发现每个 subtask 都有错的,怎么回事呢?
因为精度损失了!比如说,虽然我们要的是
6.0
6.0
6.0,但在 double 类型的变量中存储的是
5.99999
5.99999
5.99999,这就使得输出的由
6
6
6 变成了
5
5
5。于是我们需要补足精度并在此基础上不改变最后的答案。最简单的方法就是加上一个数补足精度,显然
0.001
0.001
0.001 符合要求,具体操作如以下代码:
const double eps = 1e-3
long long x = (ans + eps) * 100;
if (x % 100 == 0) printf("%lld", x / 100);
else if (x % 10 == 0) printf("%.1lf", 0.01 * x);
else printf("%.2lf", 0.01 * x);
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 100;
int n, m, s, b, f;
int head[maxn], cnt;
double ians = 2147483647, ans = 2147483647;
const double eps = 1e-3;
struct edge
{
int v, w, next;
} e[maxn << 1];
void add (int u, int v, int w)
{
e[++ cnt] = (edge) {v, w, head[u]};
head[u] = cnt;
}
struct node
{
int pos, dist;
bool operator < (node a) const
{
return a.dist < dist;
}
};
priority_queue <node> q, emptyi;
int dis[2][maxn], vis[maxn], path[maxn];
void dijkstra (int si, int tp)
{
memset (vis, 0, sizeof (vis));
memset (dis[tp], 0x7f7f7f7f, sizeof (dis[tp]));
q = emptyi;
dis[tp][si] = 0;
q.push ((node) {si, 0});
while (!q.empty ())
{
int u = q.top ().pos;
q.pop ();
if (vis[u]) continue;
vis[u] = 1;
for (int i = head[u]; i; i = e[i].next)
{
int v = e[i].v, w = e[i].w;
if (vis[v]) continue;
if (dis[tp][v] > dis[tp][u] + w)
{
dis[tp][v] = dis[tp][u] + w;
q.push ((node) {v, dis[tp][v]});
if (tp == 0)
path[v] = u;
}
else if (tp == 0 && dis[tp][v] == dis[tp][u] + w)
path[v] = min (path[v], u);
}
}
}
int main ()
{
scanf ("%d %d %d %d %d", &n, &m, &s, &b, &f);
for (int i = 1; i <= m; i++)
{
int u, v, w;
scanf ("%d %d %d", &u, &v, &w);
add (u, v, w), add (v, u, w);
}
dijkstra (f, 0);
dijkstra (b, 1);
int v = s, flag = 0;
while (v != 0)
{
if (dis[1][v] == 0x7f7f7f7f) continue;
double ansi = dis[1][v] / 3.0;
if (ansi == (dis[0][s] - dis[0][v]) / 2.0)
flag = 1, ans = min (ansi, ans);
else if (ansi * 2.0 < dis[0][s] - dis[0][v])
{
double ansii = (dis[0][s] - dis[0][v] - ansi * 2.0) / 5.0;
flag = 1, ans = min (ans, ansi + ansii);
}
else
{
if (ansi > dis[0][s] / 2.0 && flag == 0)
ians = min (ians, dis[0][v] + 3.0 * (ansi - dis[0][s] / 2.0));
else
{
double ansii = (2.0 * ansi + dis[0][v] - dis[0][s]);
if (ansii <= (dis[0][s] - 2 * ansi) / 2.0)
flag = 1, ans = min (ans, ansii + ansi);
else if (ansii > (dis[0][s] - 2 * ansi) / 2.0 && flag == 0)
ians = min (ians, dis[0][v] - 3.0 * (dis[0][s] - 2 * ansi) / 2.0);
}
}
v = path[v];
}
if (!flag) printf ("YES\n"), ans = ians;
else printf ("NO\n");
long long x = (ans + eps) * 100;
if (x % 100 == 0) printf("%lld", x / 100);
else if (x % 10 == 0) printf("%.1lf", 0.01 * x);
else printf("%.2lf", 0.01 * x);
return 0;
}
呼,这个题就做完了!
收获
- 学会找最小字典序最短路的方法,第一次见到多关键字最短路。
- 知道了精度损失问题和处理难点3的方法。
P8915 [DMOI-R2] 回到过去
难点
- 如果直接在已知t个障碍物下,去讨论 k k k 个互不相邻的块的放置个数是困难的。
- 在已知用容斥原理的情况下,如何适当地应用容斥原理,并恰当、不重不漏的枚举情况?
- 6 C n m − t 3 6C_{nm-t}^{3} 6Cnm−t3 有可能超long long,那必须 n m ( n m − 1 ) nm(nm-1) nm(nm−1) 取完余数再与 n m − 2 nm-2 nm−2 相乘再除 6 6 6,但是这样不一定可以被 6 6 6 整除了怎么办?
解决
难点 1
正如难点 1 中所说,直接在已知 t t t 个障碍物下,去讨论放置 k k k 个互不相邻的块是困难的。思考为什么会难,难在互补相邻上。于是,我们可以用容斥原理:先算出在已知 t t t 个障碍物的情况下,不考虑互不相邻,有多少种方案,不难想到是 C n m − t k C_{nm - t}^{k} Cnm−tk,然后我们减去相邻的方案数。如何计算相邻的方案数呢?容斥原理,即先算出没有障碍物时有多少种相邻的方案,再减去涉及到障碍物的方案,便是相邻的方案。
难点 2
当
k
=
2
k = 2
k=2 时,我们可知没有障碍物时有多少种相邻的方案为
n
r
=
n
(
m
−
1
)
+
m
(
n
−
1
)
nr = n(m-1)+m(n-1)
nr=n(m−1)+m(n−1)。我们记涉及黑块的两连通块数为
c
n
t
cnt
cnt,然后我们遍输入障碍物,遍判断。具体的判断方法是:如果这块障碍物(黑块)的上(或下或左或右)边相邻的那块存在且仍为白块,则
c
n
t
+
1
cnt+1
cnt+1。则最后的答案便是
n
r
−
c
n
t
nr - cnt
nr−cnt。
当
k
=
3
k =3
k=3 时,根据每个
2
∗
2
2*2
2∗2 正方形有
4
4
4 种三连通块,
3
∗
1
3*1
3∗1 或
1
∗
3
1*3
1∗3 长方形都是三连通块,可知
n
r
i
=
4
(
n
−
1
)
(
m
−
1
)
+
m
a
x
(
n
−
2
,
0
)
m
+
m
a
x
(
m
−
2
,
0
)
n
nri = 4(n-1)(m-1)+max(n-2,0)m+max(m-2,0)n
nri=4(n−1)(m−1)+max(n−2,0)m+max(m−2,0)n。记涉及黑块的三连通块数为
c
n
t
i
cnti
cnti,根据黑块在排列组合可知目前正在输入的黑块,在三连通块中有
18
18
18 种可能的占位,一一枚举即可。再用一次容斥原理,可得到最后的答案是
n
r
i
−
c
n
t
(
n
m
−
t
−
2
)
+
c
n
t
i
nri - cnt(nm - t - 2) +cnti
nri−cnt(nm−t−2)+cnti。
注:便输入便判断联合判断方法,做到了不重不漏。
难点 3
很简单,改成乘以 6 6 6 在模 1 e 9 + 7 1e9+7 1e9+7 下的乘法逆元 166666668 166666668 166666668 就可以了。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxt = 2e4 + 100;
const int MOD = 1e9 + 7;
int t, T, k;
long long n, m, ans;
map <pair <int, int>, int> vis;
bool ok (int x, int y)
{
if (x < 1 || y < 1 || x > n || y > m) return false;
if (vis.find (make_pair (x, y)) != vis.end ()) return false;
return true;
}
int main ()
{
scanf ("%d", &T);
while (T --)
{
vis.clear ();
scanf ("%lld %lld %d %d", &n, &m, &k, &t);
long long nr = (n * (m - 1) % MOD + m * (n - 1) % MOD) % MOD;
long long nri = (4ll * (n - 1) * (m - 1) % MOD + m * max (0ll, n - 2) % MOD + n * max (m - 2, 0ll) % MOD) % MOD;
for (int i = 1; i <= t; i++)
{
int x, y;
scanf ("%d %d", &x, &y);
bool a = ok (x + 1, y), b = ok (x - 1, y), c = ok (x, y + 1), d = ok (x, y - 1);
if (a) nr --;
if (b) nr --;
if (c) nr --;
if (d) nr --;
if (c && d) nri --;
if (a && b) nri --;
if (b && ok (x - 2, y)) nri --;
if (a && ok (x + 2, y)) nri --;
if (c && ok (x, y + 2)) nri --;
if (d && ok (x, y - 2)) nri --;
if (a && d) nri --;
if (a && c) nri --;
if (b && c) nri --;
if (b && d) nri --;
if (a && ok (x + 1, y + 1)) nri --;
if (a && ok (x + 1, y - 1)) nri --;
if (b && ok (x - 1, y + 1)) nri --;
if (b && ok (x - 1, y - 1)) nri --;
if (d && ok (x + 1, y - 1)) nri --;
if (d && ok (x - 1, y - 1)) nri --;
if (c && ok (x + 1, y + 1)) nri --;
if (c && ok (x - 1, y + 1)) nri --;
vis.insert (make_pair (make_pair (x, y), 1));
}
long long v = (n * m - t + MOD) % MOD;
long long num = v * (v - 1) / 2% MOD;
long long numi = v * (v - 1) % MOD * max (v - 2, 0ll) % MOD * 166666668 % MOD;
if (k == 2)
ans = (num - nr + MOD) % MOD;
if (k == 3)
ans = (numi - nr * (max (v - 2, 0ll) + MOD) % MOD + nri + MOD) % MOD;
printf ("%lld\n", ans);
}
return 0;
}
收获
- 见识到了容斥原理与容斥系数。
- 见识到了乘法逆元的一个用法。
- 这种遍输入遍判断联合判断方法,使得答案不重不漏的方法一定要学会!