2022-12-25 「DMOI」Round 2 圣诞快乐赛

P8914 [DMOI-R2] 梦境

难点

  1. 如何得到字典序最小的最短路径。
  2. 如何找到最坏情况。
  3. 如何正确输出答案。

解决

第一个难点

我们可以以 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 1425;而正确答案是: 1 → 3 → 5 1\rightarrow3\rightarrow5 135。故“正着跑”错误。

第二个难点

解决办法:
我们先从最简单的情况入手,即怪兽在小 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;
}

呼,这个题就做完了!

收获

  1. 学会找最小字典序最短路的方法,第一次见到多关键字最短路
  2. 知道了精度损失问题和处理难点3的方法。

P8915 [DMOI-R2] 回到过去

难点

  1. 如果直接在已知t个障碍物下,去讨论 k k k 个互不相邻的块的放置个数是困难的。
  2. 在已知用容斥原理的情况下,如何适当地应用容斥原理,并恰当、不重不漏的枚举情况?
  3. 6 C n m − t 3 6C_{nm-t}^{3} 6Cnmt3 有可能超long long,那必须 n m ( n m − 1 ) nm(nm-1) nm(nm1) 取完余数再与 n m − 2 nm-2 nm2 相乘再除 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} Cnmtk,然后我们减去相邻的方案数。如何计算相邻的方案数呢?容斥原理,即先算出没有障碍物时有多少种相邻的方案,再减去涉及到障碍物的方案,便是相邻的方案。

难点 2

k = 2 k = 2 k=2 时,我们可知没有障碍物时有多少种相邻的方案为 n r = n ( m − 1 ) + m ( n − 1 ) nr = n(m-1)+m(n-1) nr=n(m1)+m(n1)。我们记涉及黑块的两连通块数为 c n t cnt cnt,然后我们遍输入障碍物,遍判断。具体的判断方法是:如果这块障碍物(黑块)的上(或下或左或右)边相邻的那块存在且仍为白块,则 c n t + 1 cnt+1 cnt+1。则最后的答案便是 n r − c n t nr - cnt nrcnt
k = 3 k =3 k=3 时,根据每个 2 ∗ 2 2*2 22 正方形有 4 4 4 种三连通块, 3 ∗ 1 3*1 31 1 ∗ 3 1*3 13 长方形都是三连通块,可知 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(n1)(m1)+max(n2,0)m+max(m2,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 nricnt(nmt2)+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;
}

收获

  1. 见识到了容斥原理容斥系数
  2. 见识到了乘法逆元的一个用法。
  3. 这种遍输入遍判断联合判断方法,使得答案不重不漏的方法一定要学会!
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值