8月26日模拟赛题解

前言

\(\text{T1}\):递推,评分 \(10\)。然而 \(30min\) 才写完。\((1)\)

\(\text{T2}\):反悔贪心,评分 \(50\)。然而因为数据过水 \(\operatorname{sort}\) 两遍可过,但因为没开 \(\operatorname{long long}93\) 分。\((2)\)

\(\text{T3}\):树形 \(\rm DP\),评分 \(40\)。然而因为没时间乱写了个 \(\left\lceil\dfrac{k}{2}\right\rceil\)(竟然骗了 \(20\) 分嘿嘿嘿)。\((3)\)

\(\text{T4}\):二进制+\(\rm BFS\),评分 \(50\)。然而因为看见 \(0\le K\le4\) 写了 \(4\)\(\rm struct+dijkstra\)(剩一个没时间写)乱搞,正确性没问题但只有 \(70\) 分。\((4)\)

结合 \((1)(2)(3)(4)\) 可知:我是伞兵!

正文

\(\text{T1}\) 偶数个3

\(\text{Description}\)

求出所有的 \(n\) 位数中,有多少个数中含有偶数个数字 \(3\)\(\mod 12345\))。

\(\text{Solution}\)

当前 \(i-1\) 位确定后,第 \(i\) 位有选 \(3\) 和不选 \(3\) 两种,考虑递推。

\(dp_{i,0}\) 表示所有的 \(i\) 位数中有多少个数有偶数个 \(3\)\(dp_{i,1}\) 表示所有的 \(i\) 位数中有多少个数有奇数个 \(3\)

对于 \(dp_{i,0}\):若在第 \(i\) 位放 \(3\),则为 \(dp_{i-1,1}\);若在第 \(i\) 位不放 \(3\),则第 \(i\) 位有 \(9\) 种选择,为 \(dp_{i-1,0}\times9\)

对于 \(dp_{i,1}\):若在第 \(i\) 位放 \(3\),则为 \(dp_{i-1,0}\);若在第 \(i\) 位不放 \(3\),则第 \(i\) 位有 \(9\) 种选择,为 \(dp_{i-1,1}\times9\)

\[dp_{i,0}=dp_{i-1,1}+dp_{i-1,0}\times9 \]

\[dp_{i,1}=dp_{i-1,0}+dp_{i-1,1}\times9 \]

答案为 \(dp_{n,0}\),但是当推到第 \(n\) 位时注意前导 \(0\),需要减去 \(dp_{n-1,0}\)

初始化 \(dp_{i,0}=9,dp_{i,1}=1\)

还有一个定义上的坑:\(0\) 不是一位数,要特判。

时间复杂度为 \(\mathcal{O}(n)\)

\(\text{Code}\)

#include <iostream>
#include <cstdio>
using namespace std;

const int MOD = 12345;

int dp[1005][2];

int main()
{
	int n;
	scanf("%d", &n);
	if (n == 1)
	{
		puts("8");
		return 0;
	}
	dp[1][0] = 9;
	dp[1][1] = 1;
	for (int i = 2; i <= n; i++)
	{
		dp[i][0] = (dp[i - 1][1] + dp[i - 1][0] * 9 % MOD) % MOD;
		dp[i][1] = (dp[i - 1][0] + dp[i - 1][1] * 9 % MOD) % MOD;
	}
	printf("%d\n", (dp[n][0] - dp[n - 1][0] + MOD) % MOD);
	return 0;
}

\(\text{T2}\) 购物

\(\text{Description}\)

\(n\) 件商品,每一件商品价格为 \(p_i\)。有 \(m\) 元以及 \(k\) 张优惠券。购买第 \(i\) 件商品时,使用一张优惠券,可使该商品的价格会下降至 \(q_i\)。求至多能购买多少件商品。

\(\text{Solution}\)

首先有一个很显然的贪心:先按照 \(q_i\) 从小到大排序,用完 \(k\) 张优惠券后再把剩下的按照 \(p_i\) 从小到大排序去买。

#include <iostream>
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;

const int MAXN = 5e4 + 5;

struct node
{
	int p, q;
	bool vis = false;
}a[MAXN];

bool cmp1(node x, node y)
{
	return x.q < y.q;
}

bool cmp2(node x, node y)
{
	return x.p < y.p;
}

signed main()
{
	int n, k, m, ans = 0;
	scanf("%lld%lld%lld", &n, &k, &m);
	for (int i = 1; i <= n; i++)
	{
		scanf("%lld%lld", &a[i].p, &a[i].q);
	}
	sort(a + 1, a + n + 1, cmp1);
	bool flag = true;
	for (int i = 1; i <= n; i++)
	{
		if (m < a[i].q)
		{
			flag = false;
			break;
		}
		if (!k)
		{
			break;
		}
		k--;
		ans++;
		m -= a[i].q;
		a[i].vis = true;
	}
	if (!flag)
	{
		printf("%lld\n", ans);
		return 0;
	}
	sort(a + 1, a + n + 1, cmp2);
	for (int i = 1; i <= n; i++)
	{
		if (m < a[i].p)
		{
			break;
		}
		if (a[i].vis)
		{
			continue;
		}
		ans++;
		m -= a[i].p;
	}
	printf("%lld\n", ans); 
	return 0;
}

这个算法在赛时能 A,但 @lsw1 给出了一组 \(hack\)

2 1 11
5 4
9 6

我们会先把优惠券用在第一件上,但 \(4+9>11\),因此输出为 \(1\)。实际上把优惠券用在第二件上,答案为 \(2\)

然后就只有 \(88\) 分了 😦

这时考虑反悔贪心:在用优惠券时将 \(p_i-q_i\) 放入小根堆中,代表能用 \(p_i-q_i\) 重新买一张优惠券。

按照 \(p_i\) 排序后,对差值进行贪心。每次取出堆顶的值 \(now\),若 \(p[now]-q[now]<p[i]-q[i]\),则将 \(now\) 替换为 \(i\)

时间复杂度 \(\mathcal{O}(n\log n)\)

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#define int long long
using namespace std;

const int MAXN = 5e4 + 5;

struct node
{
	int p, q;
	bool operator <(const node &x)const
	{
		return x.p - x.q < p - q;
	}
}a[MAXN];

priority_queue<node> pq;

bool cmp1(node x, node y)
{
	return x.q < y.q;
}

bool cmp2(node x, node y)
{
	return x.p < y.p;
}

signed main()
{
	int n, k, m;
	scanf("%lld%lld%lld", &n, &k, &m);
	for (int i = 1; i <= n; i++)
	{
		scanf("%lld%lld", &a[i].p, &a[i].q);
	}
	sort(a + 1, a + n + 1, cmp1);
	for (int i = 1; i <= k; i++)
	{
		if (m < a[i].q)
		{
			printf("%lld\n", i - 1);
			return 0;
		}
		m -= a[i].q;
		pq.push(a[i]);
	}
	sort(a + k + 1, a + n + 1, cmp2);
	int ans = k;
	for (int i = k + 1; i <= n; i++)
	{
		node now = pq.top();
		if (now.p - now.q < a[i].p - a[i].q && m >= a[i].q + now.p - now.q)
		{
			m -= a[i].q + now.p - now.q; //反悔
			ans++;
			pq.pop();
			pq.push(a[i]);
		}
		else if (m >= a[i].p)
		{
			m -= a[i].p; //否则直接买
			ans++;
		}
	}
	printf("%lld\n", ans);
	return 0;
}

\(\text{T3}\) 拆网线

\(\text{Description}\)

有一棵树,要去掉一些边。但是现在有 \(k\) 个球,需要把这 \(k\) 个球放到不同的节点上,然后去掉一些边,需要保证每个球还能通过留下来的边到达至少另一个球。问最少需要保留多少条边。

\(\text{Solution}\)

贪心:一条边可以放 \(2\) 个球,这样能使剩下的边最少,尽量多构造。

假设这样能分出 \(res\) 个球:

  1. \(res\ge k\):答案就是 \(\left\lceil\dfrac{k}{2}\right\rceil\)
  2. \(res<k\):剩下 \(k-res\) 个球每个都需要一条边连向其它球,答案是 \(\left\lceil\dfrac{res}{2}\right\rceil-(k-res)\)

考虑如何求出,树上当然就用树形 \(\rm DP\) 啦。

\(dp_{u,0}\) 表示 \(\operatorname{subtree}(u)\) 中有多少个点能两两配对(不包含 \(u\));

\(dp_{u,1}\) 表示 \(\operatorname{subtree}(u)\) 中有多少个点能两两配对(包含 \(u\))。

显然有

\[dp_{u,0}=\sum\limits_{v\in son(u)}dp_{v,1}$$。 在求出 $dp_{u,0}$ 后,我们考虑 $dp_{u,1}$:对于 $u$ 的每个儿子 $v$,先用 $dp_{i,0}$ 减去 $dp_{v,1}$,这时 $v$ 的子树应选 $dp_{v,0}$,最后将 $u$ 和 $v$ 配对,多 $2$ 个,即 $$dp_{u,1}=\max\limits_{v\in son(u)}\{dp_{u,0}-dp_{v,1}+dp_{v,0}+2\}\]

最后 \(res\gets\max(dp_{i,0},dp_{i,1})\)

时间复杂度 \(\mathcal{O}(tn)\)

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

const int MAXN = 100005;

int cnt;
int head[MAXN], dp[MAXN][2];

struct edge
{
	int to, nxt;
}e[MAXN << 1];

void add(int u, int v)
{
	e[++cnt] = edge{v, head[u]};
	head[u] = cnt;
}

void dfs(int u, int fa)
{
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (v == fa)
		{
			continue;
		}
		dfs(v, u);
		dp[u][0] += dp[v][1];
	}
	for (int i = head[u]; i; i = e[i].nxt)
	{
		int v = e[i].to;
		if (v == fa)
		{
			continue;
		}
		dp[u][1] = max(dp[u][1], dp[u][0] - dp[v][1] + dp[v][0] + 2);
	}
}

int main()
{
	int t;
	scanf("%d", &t);
	while (t--)
	{
		cnt = 0;
		memset(head, 0, sizeof(head));
		memset(dp, 0, sizeof(dp));
		int n, k;
		scanf("%d%d", &n, &k);
		for (int i = 1; i < n; i++)
		{
			int fa;
			scanf("%d", &fa);
			add(i + 1, fa);
			add(fa, i + 1);
		}
		dfs(1, 0);
		int res = max(dp[1][0], dp[1][1]);
		if (res >= k)
		{
			printf("%d\n", (k + 1) >> 1);
		}
		else
		{
			printf("%d\n", (res >> 1) + k - res);
		}
	}
	return 0;
}

\(\text{T4}\) 密室

\(\text{Description}\)

\(n\) 个房间,要从 \(1\) 号房间走到 \(n\) 号房间。每一个房间中可能有一些钥匙,钥匙的种类数为 \(k\),有 \(m\) 条从房间 \(x\) 到房间 \(y\) 的单向边。想要通过某条边,就必须具备一些种类的钥匙(每种钥匙都要有才能通过)。通过这条边后, 钥匙不会消失。希望通过尽量少的边到达终点,若不能到达终点,输出 \(\text{No Solution}\),否则求出经过的边数。

\(\text{Solution}\)

为方便处理,我们将每个点有的钥匙和每条边要的钥匙压成二进制。

若现在有一些钥匙,我们怎么判断是否能通过某条边呢?

假设现在的钥匙为 \(101000\)

通过这条边要的为 \(101010\)

直接将两者相与得 \(101000\),不与 \(101010\) 相等。所以不能过去。

然后直接 \(\rm BFS\) 一遍即可。

时间复杂度 \(\mathcal{O}(2^kn)\)

\(\text{Code}\)

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;

const int MAXN = 5005;
const int MAXM = 5305;

int n, m, k, cnt;
int head[MAXN], p[MAXN];
bool vis[MAXN][1100];

struct edge
{
	int to, dis, nxt;
}e[MAXM];

void add(int u, int v, int w)
{
	e[++cnt] = edge{v, w, head[u]};
	head[u] = cnt;
}

struct node
{
	int from, dis, key;
};

void spfa()
{
	queue<node> q;
	q.push(node{1, 0, p[1]});
	while (!q.empty())
	{
		int from = q.front().from, dis = q.front().dis, key = q.front().key;
		q.pop();
		if (from == n)
		{
			printf("%d\n", dis);
			return;
		}
		if (vis[from][key])
		{
			continue;
		}
		vis[from][key] = true;
		for (int i = head[from]; i; i = e[i].nxt)
		{
			int to = e[i].to, w = e[i].dis, newk;
			if ((key & w) != w)
			{
				continue; //不能过去
			}
			newk = key | p[to]; //新钥匙
			q.push(node{to, dis + 1, newk});
		}
	}
	puts("No Solution");
}

int main()
{
	scanf("%d%d%d", &n, &m, &k);
	for (int i = 1; i <= n; i++)
	{
		for (int j = 0; j < k; j++)
		{
			int x;
			scanf("%d", &x);
			if (x)
			{
				p[i] |= (1 << j); //压位
			}
		}
	}
	for (int i = 1; i <= m; i++)
	{
		int u, v, w = 0;
		scanf("%d%d", &u, &v);
		for (int j = 0; j < k; j++)
		{
			int x;
			scanf("%d", &x);
			if (x)
			{
				w |= (1 << j); //压位
			}
		}
		add(u, v, w);
	}
	spfa();
	return 0;
}
posted @ 2021-08-26 21:02  mango09  阅读(22)  评论(0编辑  收藏  举报
-->