2021.8.26 比赛题整理

本文作者回顾了一次编程竞赛的经历,分享了四道题目及其解题思路。涉及递推公式解决数字问题、贪心策略优化购物方案、树形动态规划解决树的分割问题以及状态压缩与宽度优先搜索在图论问题中的应用。作者还反思了比赛中遇到的突发状况及应对策略。
摘要由CSDN通过智能技术生成

2021.8.26 2021CSPJ初二初一冲刺一

链接集合

18 / 32,188 / 400。

总结

被公开处刑了,麻了。T2 T4 做的不错,T1 拖后腿。

比赛刚写完 T1 代码电脑突然不行,代码没了,这突发情况真没想到。影响到了 T1。

若 T1 AC 了就 268 了。。

写着是普及,结果后三道都是提高的。。

题目概述

T1:递推 OR 组合数应用;

T2:较复杂贪心 + 优先队列;

T3:树形 dp(又是它);

T4:状压 + 宽搜。

赛时情况

T1:20 pts(杯具,耽误了一个多小时);

T2:93 pts(少了一个判断 + 可以被新数据 hack,rejudge 后就不对了);

T3:0 pts(纯瞎搞);

T4:75 pts(数据善良,成功骗分)。

T1

题意

求出所有的 n n n 位数中,有多少个数中有偶数个数字 3。

思路

一、递推

n n n 位数的情况可以由 n − 1 n-1 n1 位数推得(补一位数即可)。

注意:0 不是一位数。

0 为什么不是一位数?原因参考于《九年义务教育六年制小学数学第八册教师教学用书》。(省略一堆祖安话。

所以初始化 f 1 , 2 ← 8 f_{1,2} \gets 8 f1,28 f 1 , 1 ← 1 f_{1,1} \gets 1 f1,11

f i , 1 f_{i,1} fi,1 表示 i 位数中,3 的个数是奇数的数的总数。

f i , 2 f_{i,2} fi,2 表示 i 位数中,3 的个数是偶数的数的总数。

就可以推得:

f i , 1 ← ( f i − 1 , 2 + f i − 1 , 1 × 9 ) m o d    p f_{i,1} \gets (f_{i-1,2}+ f_{i-1,1} \times 9) \mod p fi,1(fi1,2+fi1,1×9)modp

f i , 2 ← ( f i − 1 , 1 + f i − 1 , 2 × 9 ) m o d    p f_{i,2} \gets (f_{i-1,1}+f_{i-1,2} \times 9) \mod p fi,2(fi1,1+fi1,2×9)modp

因为 0 不是一位数,所以不用考虑或减去首位为 0 的情况。

#include<bits/stdc++.h>
using namespace std;

#define int long long
const int mod = 12345;
const int maxn = 1005;
int n, f[maxn][3];

signed main ()
{
	scanf ("%lld", &n);
	f[1][2] = 8;
	f[1][1] = 1;
	for (int i = 2; i <= n; i++)
	{
		f[i][2] = (f[i - 1][1] + f[i - 1][2] * 9) % mod;
		f[i][1] = (f[i - 1][2] + f[i - 1][1] * 9) % mod;
	}
	printf ("%lld\n", f[n][2]);
	return 0;
}

二、组合数

这就是我赛时想的方法了。

不过在求组合数的时候,我山本地选择了用暴力求解,而不是预处理出 C 数组。

易得,当 n n n 位数有 k 个 3 时( k < n k < n k<n k ≡ 0 ( m o d 2 ) k \equiv 0 \pmod 2 k0(mod2)),

要么是首位有 3,即有 C n − 1 k − 1 C_{n-1}^{k-1} Cn1k1 种情况,剩余空位都有 9 种选择情况,用快速幂求出再乘上组合数即可;

要么首位没有 3,即有 C n − 1 k C_{n-1}^{k} Cn1k 种情况,然后同上(记得最后要乘上首位的 8 种选择情况)。

预处理 C 数组!!!

#include<bits/stdc++.h>
using namespace std;

#define int long long
#define rint register int
const int mod = 12345;
int n, k;
int ans;
int C[1005][1005];

inline int read ()
{
	int x = 1, s = 0;
	char ch = getchar ();
	while (ch < '0' or ch > '9'){if (ch == '-') x = -1; ch = getchar ();}
	while (ch >= '0' and ch <= '9') s = s * 10 + ch - '0', ch = getchar ();
	return x * s;
}

inline int ksm (int x, int p)
{
	int res = 1;
	while (p >= 1)
	{
		if (p & 1)
		{
			res = (res % mod * x % mod);
			p -= 1;
		}
		x = (x * x % mod);
		p /= 2;
	}
	return res;
}

signed main ()
{
	n = read ();
	if (n == 1) {printf ("8\n"); return 0;}
	for (rint i (1); i <= n; ++i)
	{
		C[1][i] = i, C[i][i] = 1, C[0][i] = 1;
		for (rint j (2); j < i; ++j)
		{
			if (C[i - j][i]) C[j][i] = C[i - j][i];
			else C[j][i] = (C[j - 1][i - 1] + C[j][i - 1]) % mod;
		}
	}
	if (n & 1) k = n - 1;
	else k = n - 2, ans = 1;
	while (k >= 2)
	{
		int tot = 1;
		tot = (tot * C[k - 1][n - 1] % mod);
		tot = (tot * ksm (9, n - k) % mod);
		int tot2 = 1;
		tot2 = (tot2 * C[k][n - 1] % mod);
		tot2 = (tot2 * ksm (9, n - k - 1) % mod * 8 % mod);
		ans = (ans + tot) % mod;
		ans = (ans + tot2) % mod;
		k -= 2;
	} 
	int kmp = ksm (9, n - 1) % mod * 8 % mod;
	ans += kmp;
	printf ("%lld\n", ans % mod);
	return 0;
}

T2

题意

买东西,有 n n n 件物品、 m m m 元以及 k k k 张优惠券。

给出每件物品的原价和使用优惠券之后的价格,问最多能买多少件物品。

思路

贪心。

但是要注意优惠券的使用。

这道题简单(只能拿部分分)的贪心思路比较简单,就不说了,直接说正解的贪心。

1

可以发现,对于优惠券,只有都用完和用了还有剩的情况

所以我们先挑优惠了之后价格小的物品用优惠券购买。

在购买的过程中,如果 m m m 不够减了(即钱用完了),直接输出已买个数即可。

若成功用优惠券购买了它,就要把它入队列。

这个队列是优先队列,排序的依据是它原价与折扣价之差的大小

2

剩下的物品已经没有优惠券可以使用了。所以我们直接对他们依据原价大小从小到大排序。

接下来我们再遍历剩下的物品。

一边遍历,一边将优先队列的队头和它比较:

如果 当前物品原价和折扣价之差 比 队头物品原价与折扣价之差 要小,

我们就可以考虑让队头的物品按原价购买,当前物品按折扣价购买

原因:

若我们一定要购买这两个物品,我们的优惠券一定要用在最有用的物品上——即原价与折扣价差值较大的物品。

这里给出一个 hack 数据 (就是因为它这道题才这么恶心的)

2 1 11
9 6
5 4
注意这时候优惠券就必须要用在第一个物品上了。

如果 m ≥ a i . y + u . x − u . y m \ge a_i.y + u.x - u.y mai.y+u.xu.y a i a_i ai 为当前物品, u u u 为队头;x 为原价,y 为折扣价),

即可以让当前物品按折扣价购买,就可以购买当前物品,

弹出队头(队头已经变成用原价购买了),当前物品入队(它用优惠券购买了)。

反之,若前面条件无法满足,就可以考虑是否能用原价购买当前物品了。

每次购买就 c n t ← c n t + 1 cnt \gets cnt + 1 cntcnt+1,最后输出 cnt 即可。

3

通过一顿分析,可以知道:

优先队列储存的是用优惠券购买了的物品。

每次出队入队的操作可以理解为:优惠券换个物品用的过程。

之所以用优先队列,是为了保证在可以购买的前提下让每张优惠券发挥它的最大作用

以此减少花费,留出更多的钱去购买其他物品。

上面这句话的后半句就有了贪心的味道了,所以这道题本质上,就是贪心而已。

代码


#include<bits/stdc++.h>
using namespace std;

#define int long long
#define rint register int
const int maxn = 5e4 + 5;
struct node{
	int x, y;
}a[maxn];
bool operator < (const node &p, const node &q)
{
	return p.x - p.y > q.x - q.y;
}
int n, k, m;
priority_queue <node> pq;

bool cmp (node a, node b)
{
	return a.y < b.y;
}

bool cmp2 (node a, node b)
{
	return a.x < b.x;
}

signed main ()
{
	scanf ("%lld %lld %lld", &n, &k, &m);
	for (rint i (1); i <= n; ++i) scanf ("%lld%lld", &a[i].x, &a[i].y);
	sort (a + 1, a + n + 1, cmp);
	for (rint i (1); i <= k; ++i)//把优惠券都用掉
	{
		if (m >= a[i].y)
		{
			pq.push ({a[i].x, a[i].y});
			m -= a[i].y;
		}
		else
		{
			printf ("%lld\n", i - 1);
			return 0;
		}
	}
	int cnt = k;
	sort (a + k + 1, a + n + 1, cmp2);
	for (rint i (k + 1); i <= n; ++i)
	{
		node u = pq.top ();
		if (a[i] < u and m >= a[i].y + u.x - u.y)
		{
			cnt++;
			m -= (a[i].y + u.x - u.y);
			pq.pop ();
			pq.push (a[i]);//换个物品用优惠券
		}
		else if (m >= a[i].x)
		{
			cnt++;
			m -= a[i].x;
		}
	}
	printf ("%lld\n", cnt);
	return 0;
}

T3

题意

给一棵含有 n n n 个节点的树,求至少保留多少边,使得 k k k 个节点每个至少能和一个节点相连。

思路

1

易得,每两个一组,即两点一线的块,是最优情况。

所以我们的目的是要将树尽量多地分成若干组点,两两一组

接下来先考虑另一个条件限制 k。

设我们已求出树最多可以被分为 a n s > > 1 ans>>1 ans>>1 对点,若 a n s ≥ k ans \ge k ansk,最少就要连接 k + 1 > > 1 k+1>>1 k+1>>1 条边。

但若是 a n s < k ans<k ans<k,对于那些无法被分入组中的节点,单独给他们一条边即可,即 ( a n s > > 1 ) + ( k − a n s ) (ans>>1)+(k-ans) (ans>>1)+(kans)

2

如何求出一棵树的 ans 的最大值呢?

树形 dp。

对于每一个节点,定义两个状态:

d p i , 0 dp_{i,0} dpi,0,表示 i 的子树(不含 i)最多能被分成多少组;

d p i , 1 dp_{i,1} dpi,1,表示 i 的子树(含 i)最多能被分成多少组。

易得 ans 就是 max ⁡ d p 1 , 0 , d p 1 , 1 \max dp_{1,0},dp_{1,1} maxdp1,0,dp1,1

状态转移方程(j 为 i 的子节点):

d p i , 0 ← ∑ d p j , 1 dp_{i,0} \gets \sum\limits_{}^{} dp_{j,1} dpi,0dpj,1

d p i , 1 ← max ⁡ d p i , 1 , d p i , 0 − d p j , 1 + d p j , 0 + 2 dp_{i,1} \gets \max dp_{i,1},dp_{i,0} - dp_{j,1} + dp_{j,0} + 2 dpi,1maxdpi,1,dpi,0dpj,1+dpj,0+2

又是树形 dp。。

代码

#include<bits/stdc++.h>
using namespace std;

const int maxn = 100005;
int T, n, k;
int dp[maxn][3];
int cnt, hd[maxn];
struct node{
	int to, nxt;
}e[maxn * 2];
int tot;
int ans;

inline int read ()
{
	int x = 1, s = 0;
	char ch = getchar ();
	while (ch < '0' or ch > '9'){if (ch == '-') x = -1; ch = getchar ();}
	while (ch >= '0' and ch <= '9') s = s * 10 + ch - '0', ch = getchar ();
	return x * s;
}

inline void add (int u, int v)
{
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}

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

int main ()
{
	T = read ();
	while (T--)
	{
		ans = cnt = 0;
		memset (dp, 0, sizeof dp);
		memset (hd, 0, sizeof hd);
		memset (e, 0, sizeof e);
		n = read (), k = read ();
		for (int i = 1; i < n; i++)
		{
			int u;
			u = read ();
			add (u, i + 1);
		}
		dfs (1, -1);
		ans = max (dp[1][1], dp[1][0]);
		if (ans >= k) printf ("%d\n", k + 1 >> 1);
		else printf ("%d\n", ans / 2 + (k - ans));
	}
	return 0;
}

T4

题意

给定一个 n n n 个节点、 m m m 条边的有向图,每走一条边就需要它要的钥匙(只看种类),每到一个节点就可以收集它有的钥匙。

问最少经过多少条边才能从 1 到达终点 n n n

思路

走的话直接跑个 bfs 就行。

关键是存、比较、结合每种钥匙的拥有情况。

这里用到了状压。

把对钥匙的拥有情况转化成一个二进制数,每次传递的时候就用十进制。

在二进制中,第 i 个数位为 0 代表不拥有第 i 把钥匙,1 代表拥有第 i 把钥匙。

  1. 在最初赋值(输入时)每一个节点时,每新输入一种钥匙的拥有状态就把之前的数左移一数位。

    左移一位,即 x < < 1 x<<1 x<<1,也就是 x × 2 x \times 2 x×2

  2. 每次判断当前边是否能走时,就用 AND & 与运算——只有当两数同位都为 1(一个需要一个拥有)时,才为 1.

    然后再拿与过的数再次和当前边要求的拥有状态进行比较,如果一样,即可以走。

  3. 每次结合两个拥有状态,用 OR | 或运算——两数同位一方为 1 即为 1,否则为 0。

剩下的就都不难了 QwQ。

代码

#include<bits/stdc++.h>
using namespace std;

#define rint register int 
const int maxn = 5005;
int n, m, k;
int cnt, hd[maxn];
struct node{
	int to, nxt, w;
}e[maxn * 2];
int dis[maxn], vis[maxn][maxn];
struct que{
	int u, stp, w;
};
queue <que> q;
int val[maxn];

inline int read ()
{
	int x = 1, s = 0;
	char ch = getchar ();
	while (ch < '0' or ch > '9'){if (ch == '-') x = -1; ch = getchar ();}
	while (ch >= '0' and ch <= '9') s = s * 10 + ch - '0', ch = getchar ();
	return x * s;
}

inline void add (int u, int v)
{
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}

inline void bfs (int s, int t)
{
	memset (dis, 1e4, sizeof dis);
	dis[s] = 0;
	q.push ((que){1, 0, val[1]});
	while (!q.empty ())
	{
		int u = q.front ().u;
		int s = q.front ().stp;
		int w = q.front ().w;
		q.pop ();
		if (u == n)
		{
			printf ("%d\n", s);
			return;
		}
		if (vis[u][w]) continue;
		vis[u][w] = 1;
		for (int i = hd[u]; i; i = e[i].nxt)
		{
			int v = e[i].to;
			if ((w & e[i].w) == e[i].w)
			{
				q.push ({v, s + 1, w | val[v]});
			}
		}
	}
	printf ("No Solution\n");
}

int main ()
{
	n = read (), m = read (), k = read ();
	for (rint i (1); i <= n; ++i) 
	for (rint j (1); j <= k; ++j)
	{
		int x;
		x = read ();
		val[i] = val[i] * 2 + x;
	}
	for (rint i (1); i <= m; ++i)
	{
		int u, v;
		u = read (), v = read ();
		add (u, v);
		for (rint j (1); j <= k; ++j)
		{
			int x;
			x = read ();
			e[cnt].w = e[cnt].w * 2 + x;
		}
	}
	bfs (1, n);
	return 0;
}

除了突发事件,其他状态都不错~

明天考试 rp++~ ——21.08.26

—— E n d End End——

阳 和 启 蛰 , 枯 木 逢 春 。 阳和启蛰,枯木逢春。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值