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_{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\) 个球:
- \(res\ge k\):答案就是 \(\left\lceil\dfrac{k}{2}\right\rceil\);
- \(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\))。
显然有
最后 \(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;
}