其他分享
首页 > 其他分享> > gdfzoj 比赛题解

gdfzoj 比赛题解

作者:互联网

前言

本次比赛:初一训练5.21 / 编号531

题目难度中等偏上,有几题比较简单,有两三题较难。

T1

题目gdfzoj1441

思路

算是一道暴力题。

由于 \(h_{i, j}\) 范围很小,考虑二分答案。

二分答案的范围应该是 \([0, 110]\)。

对于 chk() 函数,可以暴力枚举所有差为 \(\texttt{mid}\) 的数对,并使用 bfs 强行搜索检验。

bfs 比较容易实现,重点在于 chk() 的枚举。

我们明显可以枚举所有数对的较小值,较大值自然就是 \(\text{较小值} + \texttt{mid}\)。

但是直接这样枚举太过鲁莽,可以剪枝。

注意到 \(h_{1, 1}\) 与 \(h_{n, n}\) 是必定会经过的。

数对较大值 的 最小值 应该是 \(\max(h_{1, 1}, h_{n, n})\)。

因此可得,数对较小值 的 最小值 应该是 \(\max(h_{1, 1}, h_{n, n}) - \texttt{mid}\)。

再来看 数对较小值 的 最大值 ,它是 \(\min(h_{1, 1}, h_{n, n})\)。这个稍微有点难想。

思路总结

  1. 读入数据。
  2. 写一份 bfs,参数有 \(\texttt{最高点}\) 与 \(\texttt{最低点}\),用于检验这种情况是否可行。
  3. 写二分答案必须用到的 chk() 函数,实现如上所述,思考上略有困难。
  4. 写二分,然后输出。

代码

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#define N 105
using namespace std;
int n, a[N][N];
int Max, Min;
void Input()
{
	scanf("%d", &n);
	if (n == 1) //特判
	{
		printf("0");
		exit(0);
	}
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			scanf("%d", &a[i][j]);
	Max = max(a[1][1], a[n][n]);
	Min = min(a[1][1], a[n][n]);
}
struct Node
{
	int x, y;
};
int dict[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
bool vis[N][N];
bool bfs(int minn, int maxn) //非常模板的 bfs 只是要判一下数的范围。
{
	memset(vis, false, sizeof(vis)); //切记清空数组
	queue <Node> Q;
	Q.push( (Node){1,1} );
	vis[1][1] = true;
	while (!Q.empty())
	{
		int x = Q.front().x, y = Q.front().y;
		Q.pop();
		if (x == n && y == n) return true;
		for (int i = 0; i < 4; i++)
		{
			int dx = x + dict[i][0], dy = y + dict[i][1];
			if (!(1 <= dx && dx <= n && 1 <= dy && dy <= n)) continue;
			if (vis[dx][dy]) continue;
			if (minn <= a[dx][dy] && a[dx][dy] <= maxn) //唯一和板子不一样的地方。
			{
				vis[dx][dy] = true;
				Q.push( (Node){dx, dy} );
			}
		}
	}
	return false;
}
bool chk(int ans) //较难理解。
{
	int l = Max - ans, r = Min;
	for (int i = l; i <= r; i++)
		if (bfs(i, i + ans))
			return true;
	return false;
}
int FIND()
{
	int pos = -1, L = 0, R = 110;
	while (L < R)
	{
		int mid = (L + R) >> 1;
		if (chk(mid)) R = mid, pos = R; //答案求最大值,pos 应该在 R 这一边更新。
		else L = mid + 1;
	}
	return pos;
}
int main()
{
	Input();
	printf("%d", FIND());
	return 0;
}

T2

题目gdfzoj1942

思路

个人觉得这题最难。

重要的前置知识:分数取模。

由于篇幅原因,不详细证明,使用到了 费马小定理 与 逆元。

这里只给出结论:\(\dfrac{x}{y}\equiv x \cdot y^{p-2} \pmod{p}\)

通过样例得知,先手赢一场比赛的概率为 \(\dfrac{2}{3}\)。

进一步地,我们设先手赢第 \(n\) 场比赛的概率为 \(p_n\),则先手输掉第 \(n\) 场比赛的概率为 \((1 - p_n)\)。

容易写出状态转移方程(假设『我』代表先手,『他』代表后手):

\[\text{我赢的概率} = \text{上一场我赢的概率} \times \text{这一场他输的概率} +\text{上一场我输的概率} \times \text{这一场我赢的概率} \]

转换成数学语言:

\[\begin{aligned}p_i & = p_{i-1} \times \dfrac{1}{3} + (1 - p_{i-1}) \times \dfrac{2}{3}\\ & = p_{i-1} \times \dfrac{1}{3} + \dfrac{2}{3} - p_{i-1} \times \dfrac{2}{3}\\ & = \dfrac{2}{3} - \dfrac{1}{3} \times p_{i-1}\end{aligned} \]

整合一下,标准的状态转移方程就是:

\[p_i = \begin{cases}\dfrac{2}{3} & i = 1\\\\\dfrac{2}{3} - \dfrac{1}{3} \times p_{i-1} & i \ge 2\end{cases} \]

但是这样的效率是 \(O(n)\) 的,还需要优化。

最快的办法是找规律,如下。

场数 先手获胜的概率 后手获胜的概率
\(1\) \(\dfrac{2}{3}\) \(\dfrac{1}{3}\)
\(2\) \(\dfrac{4}{9}\) \(\dfrac{5}{9}\)
\(3\) \(\dfrac{14}{27}\) \(\dfrac{13}{27}\)
\(4\) \(\dfrac{40}{81}\) \(\dfrac{41}{81}\)

规律如下,其实还是比较容易找的。

若当前是第 \(n\) 场比赛,则:

进一步地,可以得到下面这条式子:

\[p_n = \begin{cases} \dfrac{(3^n + 1)}{2 \times 3^n} & n\equiv1\pmod{2}\\ \\ \dfrac{(3^n - 1)}{2 \times 3^n} & n\equiv0\pmod{2}\end{cases} \]

然后对着它打代码就完事了,照着 $$\dfrac{x}{y}\equiv x \cdot y^{p-2} \pmod{p}$$ 这条结论模即可。

需要注意,此处需要使用快速幂来达到 \(O(\log n)\) 的速度求 \(3^n\)。

代码

#include <iostream>
#include <cstdio>
#define LL long long
using namespace std;
LL ksm(LL x, int y, int p) //x^y mod p
{
	if (y == 1) return x % p;
	LL t = ksm(x, y >> 1, p);
	t = (t * t) % p;
	if (y & 1) t = (t * x) % p;
	return t;
}
int main()
{
	int n, p = 998244353;
	scanf("%d", &n);
	if (n & 1)
	{
		LL POW = ksm(3, n, p);
		LL x = (POW + 1) % p, y = (2 * POW) % p;
		y = ksm(y, p-2, p);
		printf("%lld", (x * y) % p);
	}
	else
	{
		LL POW = ksm(3, n, p);
		LL x = (POW - 1) % p, y = (2 * POW) % p;
		y = ksm(y, p-2, p);
		printf("%lld", (x * y) % p);
	}
	return 0;
}

T3

题目gdfzoj2232

思路

T2 有点难啊,T3 轻松多了。

我们都知道,合法的括号序列有一个特征:

对于本题,我们可以先确认更改的字符是 ( 还是 )

假设需要更改左括号(如果是右括号,只需要倒着扫)。

统计前缀和,如果某个前缀和出现了负数,说明可以更改从开头至当前位置所有左括号

代码实现上,如果需要更改右括号,可以将左括号改成右括号,并将字符串翻转。

这样,代码就具有良好的扩展性了。

代码

#include <iostream>
#include <cstdio>
#include <string>
#include <algorithm>
using namespace std;
string s;
int len, lcnt, rcnt;
void Input()
{
	cin >> s;
	len = s.length(), lcnt = 0, rcnt = 0;
	for (int i = 0; i < len; i++)
	{
		if (s[i] == '(') lcnt++;
		else rcnt++;
	}	
}
void Reverse()
{
	//每个字符翻转+整串翻转 
	for (int i = 0; i < len; i++)
	{
		if (s[i] == '(') s[i] = ')';
		else s[i] = '(';
	}
	reverse(s.begin(), s.end());
}
void solve()
{
	int sum = 0, cnt = 0;
	for (int i = 0; i < len; i++)
	{
		if (s[i] == '(') sum++;
		else sum--, cnt++;
		if (sum < 0)
		{
			printf("%d", cnt); 
			return;
		}
	}
	printf("0");
}
int main()
{
	Input();
	if (lcnt > rcnt) Reverse();
	solve();
	return 0;
}

T4

题目gdfzoj2233

思路

没有思路,这是一道暴力题。

由于规模较小,采用 dfs 更优。

一遍深搜,一遍记录当前走过的字符串。

写一个 chk() 函数判断字符串是否为完美字符串即可。

代码

#include <iostream>
#include <cstdio>
using namespace std;
char a[7][7];
bool vis[7][7];
int maxn, dict[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
bool chk(string s)
{
	int len = s.length();
	if (len & 1) return false;
	int tlen = len >> 1;
	for (int i = 0; i < tlen; i++)
		if (s[i] == ')')
			return false;
	for (int i = tlen; i < len; i++)
		if (s[i] == '(')
			return false;
	return true;
}
int n;
void dfs(int x, int y, string s)
{
	if (chk(s))
	{
		int len = s.length();
		maxn = max(maxn, len);
		return;
	}
	for (int i = 0; i < 4; i++)
	{
		int dx = x + dict[i][0], dy = y + dict[i][1];
		if (!(1 <= dx && dx <= n && 1 <= dy && dy <= n)) continue;
		if (vis[dx][dy]) continue;
		vis[dx][dy] = true;
		dfs(dx, dy, s + a[dx][dy]);
		vis[dx][dy] = false;
	}
}
int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			cin >> a[i][j];
	vis[1][1] = true;
	string tql = ""; tql += a[1][1];
	dfs(1, 1, tql);
	printf("%d", maxn);
	return 0;
}

T5

题目gdfzoj2234

本题名声过大,了解过括号序列的人肯定做过这题。

故,不细述。

T6

题目gdfzoj2235

思路

此题明显是最短路。

我们需要求任意两点的最短路的最大值,即全源最短路。

本题共有 \(n^2\) 个点,如果跑 Floyd 时间复杂度是 \(O(n^6)\),非常极限,不是本题正解。

但是,我又不会写 Johnson 全源最短路,怎么办呢?

每个点朝四周建边,容易发现,边的数量不超过 \(4 \times n^2\)。

想到这里,明显可以跑 \(n^2\) 次 dijkstra 实现,因为当边数较小时,跑多次 dijkstra 会比 Floyd 快。

此处 dijkstra 是使用优先队列实现。

那么就很容易写出代码了。

思路总结

  1. 读入数据。
  2. 每个点朝四周建边。
  3. 写一个 dijkstra 的模版。
  4. 统计最大值,输出。

代码

#include <iostream>
#include <cstdio>
#include <queue>
#define N 905
#define M 7205 //905 * 4 * 2 = 7200
using namespace std;
int n, nn, A, B;
int dict[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
bool a[35][35];
struct Node
{
	int now, nxt, w;
}e[M];
int head[N], cur;
void add(int x, int y, int k)
{
	e[++cur].now = y;
	e[cur].nxt = head[x];
	e[cur].w = k;
	head[x] = cur;
}
struct Dis
{
	int pos, len;
	bool operator <(const Dis &t) const
	{
		return len > t.len; 
	}
}dis[N];
bool vis[N];
int dijkstra(int first) 
{
	priority_queue <Dis> Q;
	for (int i = 1; i <= nn; i++) dis[i].pos = i, dis[i].len = 2147483647, vis[i] = false;
	dis[first].len = 0;
	Q.push(dis[first]);
	while (!Q.empty())
	{
		int topi = Q.top().pos;
		Q.pop();
		if (vis[topi]) continue;
		vis[topi] = true;
		for (int i = head[topi]; i; i = e[i].nxt)
			if (dis[topi].len + e[i].w < dis[e[i].now].len)
			{
				dis[e[i].now].len = dis[topi].len + e[i].w;
				Q.push(dis[e[i].now]);
			}
	}
    //此处有是惟一与普通模版不同的地方,需要找出最大值。
	int maxn = 0;
	for (int i = 1; i <= nn; i++) maxn = max(maxn, dis[i].len);
	return maxn;
}
void Input()
{
	scanf("%d%d%d", &n, &A, &B);
	nn = n * n;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
		{
			char x;
			cin >> x;
			if (x == '(') a[i][j] = true;
		}
}
void get_edge()
{
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			for (int k = 0; k < 4; k++)
			{
				int x = i + dict[k][0], y = j + dict[k][1];
				if (!(1 <= x && x <= n && 1 <= y && y <= n)) continue;
				int t1 = n * (i-1) + j, t2 = n * (x-1) + y;
				if (a[i][j] == a[x][y]) add(t1, t2, A);
				else add(t1, t2, B);
			}
}
void solve()
{
	int maxn = 0;
	for (int i = 1; i <= nn; i++) maxn = max(maxn, dijkstra(i));
	printf("%d", maxn);
}
int main()
{
	Input();
	get_edge();
	solve();
	return 0;
}

T7

题目gdfzoj2958

思路

dp 题,较简单。

注意到数据范围 \(n \le 1000\),容易想到时间复杂度大约是 \(O(n^2)\) 左右的级别。

设 \(dp_i\) 表示:第 \(i\) 个蒟蒻留下的最多人数。

容易想到转移方程(表达式可能不太规范):

\[dp_i = \huge[\small\max\limits_{j=1}^{i-1} dp_j \space(\text{满足} |a_i-a_j| \ne 1) \huge] \small + 1 \]

貌似代码比这东西容易理解多了。

代码

#include <iostream>
#include <cstdio>
#include <cmath>
#define N 1005
using namespace std;
int a[N], dp[N];
int main()
{
	int n;
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), dp[i] = 1; //注意初始化。
	for (int i = 1; i <= n; i++)
		for (int j = 1; j < i; j++)
			if (abs(a[i] - a[j]) != 1)
				dp[i] = max(dp[i], dp[j] + 1);
	printf("%d", dp[n]);
	return 0;
}

结语

感觉这次比赛比较综合。

搜索、二分、dp、最短路、数学题、括号序列,这几种常考题型及算法都出了。

还要继续加油!

首发:2022-05-27 13:23:18

标签:return,比赛,int,题解,gdfzoj,len,++,dfrac,include
来源: https://www.cnblogs.com/liangbowen/p/16622879.html