A.Exchange(贪心)
题意:
日本有六种流通硬币: 1 1 1日元、 5 5 5日元、 10 10 10日元、 50 50 50日元、 100 100 100日元和 500 500 500日元。请回答下列有关这些硬币的问题。
AtCoder先生的钱包里有 A A A个 1 1 1日元硬币, B B B个 5 5 5日元硬币, C C C个 10 10 10日元硬币, D D D个 50 50 50日元硬币, E E E个 100 100 100日元硬币和 F F F个 500 500 500日元硬币。
他计划依次在 N N N家商店购物。具体来说,他计划在第 i i i个商店 ( 1 ≤ i ≤ N ) (1\leq i\leq N) (1≤i≤N)购买一件价格为 X i X_i Xi日元(含税)的商品。
给零钱和收零钱都需要时间,因此他想选择硬币,以便在每家商店都能正好支付所需要的金额。
请判断这是否可行。
分析:
本题考虑贪心,优先用价值较高的硬币去支付,同时将商品价格从大到小进行排序,遍历一遍即可。
代码:
#include<bits/stdc++.h>
typedef long long LL;
using namespace std;
const LL N = 25;
struct number {
int w, v;
} num[N];
int n, shop[N], tot, need;
bool cmp(int i, int j) {
return i > j;
}
void solve() {
for (int i = 1; i <= 6; i++) {
cin >> num[i].v;
tot += num[i].v * num[i].w;
}
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> shop[i];
need += shop[i];
}
if (tot < need) {
cout << "No" << endl;
return;
}
sort(shop + 1, shop + n + 1, cmp);
for (int i = 1; i <= n; i++)
for (int j = 6; j >= 1; j--) {
if (!shop[i])
break;
while (num[j].v > 0 && shop[i] - num[j].w >= 0) {
shop[i] -= num[j].w;
num[j].v--;
}
}
bool flag = false;
for (int i = 1; i <= n; i++)
if (shop[i]) {
flag = true;
break;
}
if (flag)
cout << "No" << endl;
else
cout << "Yes" << endl;
}
int main() {
num[1].w = 1, num[2].w = 5, num[3].w = 10, num[4].w = 50, num[5].w = 100, num[6].w = 500;
solve();
return 0;
}
B.Puzzle of Lamps(规律)
题意:
AtCoder先生创建了一个由 N N N个小灯泡(从左到右排列成一排)和两个开关 A A A和 B B B组成的装置:“0”(关)和"1"(开)。按下每个开关会产生以下结果:
- 按下开关 A A A会将最左边处于"0"状态的灯泡变成"1"。
- 按下开关 B B B会将处于"1"状态的最左边灯泡变为"0"。
如果没有适用的灯泡,则无法按下开关。
最初,所有灯泡都处于"0"状态。他希望灯泡的状态从左到右为 S 1 , S 2 , … , S N S_1,S_2,\dots,S_N S1,S2,…,SN。请确定按下开关的顺序和次数。按下的次数不一定要最少,但最多应为 1 0 6 10^6 106,以便在实际时间内完成操作。可以证明,在该问题的约束条件下存在一个解。
分析:
注意到开关是规定从左至右依次控制灯。
意思就是,若单独讨论 i i i位置上的灯为状态 1 1 1,它必然需要进行 i i i次操作 A A A得到。显然,在 n n n位置的灯满足这个条件。
而在之前的灯的状态可能会被后边的灯的状态所影响,分类讨论即可。
设当前 i i i位置状态为 p p p,先前灯的状态为 q q q
若 p = 0 p=0 p=0,
- 若 q = 0 q=0 q=0,直接继承,无需操作;
- 若 q = 1 q=1 q=1,需要进行 i i i次操作 B B B使 i i i灯泡由"1"变为"0";
若 p = 1 p=1 p=1,
- 若 q = 1 q=1 q=1, 直接继承,无需操作;
- 若 q = 0 q=0 q=0,需要进行 i i i次操作 A A A使 i i i号灯泡由"0"变为"1";
代码:
#include<bits/stdc++.h>
typedef long long LL;
using namespace std;
const LL N = 1e6 + 10;
int n, cnt;
string s;
char ans[N];
void solve() {
cin >> n >> s;
int last = -1;
for (int i = n - 1; i >= 0; i--) {
if (i == n - 1) {
if (s[i] == '0')
last = 0;
if (s[i] == '1') {
last = 1;
for (int k = 1; k <= n; k++)
ans[++cnt] = 'A';
}
} else {
if (s[i] == '0') {
if (last == 0)
continue;
else {
last = 0;
for (int k = 1; k <= i + 1; k++)
ans[++cnt] = 'B';
}
} else {
if (last == 1)
continue;
else {
last = 1;
for (int k = 1; k <= i + 1; k++)
ans[++cnt] = 'A';
}
}
}
}
cout << cnt << endl;
for (int i = 1; i <= cnt; i++)
cout << ans[i];
cout << endl;
}
int main() {
solve();
return 0;
}
C.Routing(BFS、最短路)
题意:
有一个
N
N
N行和
N
N
N列的网格。设
(
i
,
j
)
(i,j)
(i,j)
(
1
≤
i
≤
N
,
1
≤
j
≤
N
)
(1\leq i\leq N,1\leq j\leq N)
(1≤i≤N,1≤j≤N)表示位于从上往下第
i
i
i行和从左往上第
j
j
j列的单元格。每个单元格最初都被涂成红色或蓝色,如果
c
i
,
j
=
c_{i,j}=
ci,j=R
,表示单元格
(
i
,
j
)
(i,j)
(i,j)是红色的,如果
c
i
,
j
=
c_{i,j}=
ci,j=B
,表示这个单元格是蓝色的。现在想将某些单元格的颜色改为紫色,以便同时满足以下两个条件:
条件1:从单元格 ( 1 , 1 ) (1,1) (1,1)移动到单元格 ( N , N ) (N,N) (N,N)时,只能经过红色或紫色的单元格。
条件2:只需经过蓝色或紫色单元格,即可从单元格 ( 1 , N ) (1,N) (1,N)移动到单元格 ( N , 1 ) (N,1) (N,1)。
这里的可以移动是指可以通过重复移动到水平或垂直相邻的相关颜色的单元格,从起点到达终点。
要满足这些条件,最少有多少个单元格必须变为紫色?
分析:
使用两次BFS或者最短路算法,分别从两个方向开始搜索,把网格看成图,每个点都和四个方向联通,联通时通过所需的距离取决于颜色是否符合要求,如果需要改变颜色,就是距离等于一;反之,等于零。把这个图搜一遍或跑一遍,求出 ( 1 , 1 ) (1,1) (1,1)到 ( N , N ) (N,N) (N,N)最少经过多少蓝色,以及 ( 1 , N ) (1,N) (1,N)到 ( N , 1 ) (N,1) (N,1)最少经过多少红色,相加即答案。
代码:
#include<bits/stdc++.h>
typedef long long LL;
using namespace std;
const LL N = 505;
char s[N][N];
int n, b[N][N], r[N][N];
bool vis[N][N];
int xz[] = {1, 0, -1, 0};
int yz[] = {0, 1, 0, -1};
struct node {
int x, y, d;
};
bool operator<(node a, node b) {
return a.d > b.d;
}
priority_queue<node> q;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%s", s[i] + 1);
memset(b, 0x3f, sizeof(b));
memset(r, 0x3f, sizeof(r));
b[1][1] = (s[1][1] == 'B');
q.push({1, 1, b[1][1]});
while (!q.empty()) {
node now = q.top();
q.pop();
if (vis[now.x][now.y])
continue;
vis[now.x][now.y] = 1;
if (now.x == n && now.y == n)
continue;
for (int i = 0; i < 4; i++) {
int xx = now.x + xz[i];
int yy = now.y + yz[i];
if (xx < 1 || xx > n || yy < 1 || yy > n)
continue;
if (b[xx][yy] > now.d + (s[xx][yy] == 'B')) {
b[xx][yy] = now.d + (s[xx][yy] == 'B');
q.push({xx, yy, b[xx][yy]});
}
}
}
memset(vis, 0, sizeof(vis));
r[n][1] = (s[n][1] == 'R');
q.push({n, 1, r[n][1]});
while (!q.empty()) {
node now = q.top();
q.pop();
if (vis[now.x][now.y])
continue;
vis[now.x][now.y] = 1;
if (now.x == 1 && now.y == n)
continue;
for (int i = 0; i < 4; i++) {
int xx = now.x + xz[i];
int yy = now.y + yz[i];
if (xx < 1 || xx > n || yy < 1 || yy > n)
continue;
if (r[xx][yy] > now.d + (s[xx][yy] == 'R')) {
r[xx][yy] = now.d + (s[xx][yy] == 'R');
q.push({xx, yy, r[xx][yy]});
}
}
}
printf("%d\n", b[n][n] + r[1][n]);
return 0;
}
D.Earthquakes(单调栈、线段树)
题意:
AtCoder街是一条在平地上用直线表示的道路。路上竖立着 N N N根电线杆,高度为 H H H。电线杆按时间顺序编号为 1 , 2 , … , N 1,2,\dots,N 1,2,…,N。电线杆 i i i( 1 ≤ i ≤ N 1\leq i\leq N 1≤i≤N)垂直于坐标 X i X_i Xi。每根电线杆的底座都固定在地面上。
街道将经历 N N N次地震。在第 i i i次地震 ( 1 ≤ i ≤ N ) (1\leq i\leq N) (1≤i≤N)中,会发生以下事件:
- 如果电线杆 i i i尚未倒下,它将倒向左边或右边,每个概率为 1 2 \frac{1}{2} 21。
- 如果一根倒下的电线杆与另一根尚未倒下的电线杆相撞(包括在电线杆底部相撞),后一根电线杆也会朝同一方向倒下。这可能会引发连锁反应。
在步骤1中,一根电线杆倒下的方向与其他电线杆倒下的方向无关。
下图是在一次地震中电线杆可能倒下的示例:
为了防备地震,对于每个 t = 1 , 2 , … , N t=1,2,\dots,N t=1,2,…,N,求出在第 t t t次地震中所有极点都倒下的概率。将其乘以 2 N 2^N 2N,结果对 998244353 998244353 998244353取模。可以证明要输出的值是整数。
分析:
我们发现,所有的电线杆可以被划分为若干段。
定义一段为左端点的电线杆向右倒能让整段电线杆全部倒完的极长子区间。
不同段之间不会有任何影响,所以对于不存在连锁反应的区间,每个区间可以独立处理。因此,我们可以将原问题拆分为子问题:
有一段长度为 l l l位置升序的电线杆。
从左往右第 i i i个电线杆在第 p i p_i pi次地震中倒塌,求最后倒塌的电线杆是第 i i i个电线杆的概率。
我们发现任何时刻,一段区间均可被分成三部分:
- 向左倒塌的一部分
- 站立的一部分
- 向右倒塌的一部分
第 i i i个电线杆未倒塌,当且仅当:所有 p p p中 1 1 1到 i i i的前缀最小值都向左倒塌;所有 p p p中 l l l到的 i i i后缀最小值都向右倒塌。
这些代表了在 i i i之前主动倒塌的电线杆(不是被其他推倒的)。
当且仅当 i i i未倒塌且 i i i是站立的一段的起点或终点,第 i i i个电线杆最后倒塌。
我们发现,第 i i i个电线杆是最后倒塌的概率为 1 2 a × b 2 \frac{1}{2^a}\times \frac{b}{2} 2a1×2b。
其中 a a a为 p p p到 i i i的前缀最小值和后缀最小值的个数之和。
因为必须保持 i i i站立,所以左边的必须往左倒,右边的必须往右倒,概率为 1 2 a \frac{1}{2^a} 2a1。
若 i i i为站立区间的左端点或右端点, b = 1 b=1 b=1。
若 i i i是单独的一个(即既是左端点又是右端点), b = 2 b=2 b=2。
若 i i i是左右端点中的一个,则 i i i倒下的方向有要求,概率为 1 2 \frac{1}{2} 21。
若 i i i同时是左右端点(单独),则 i i i倒下的方向没有要求,概率为 1 1 1。
不难发现,概率中
a
a
a的求解过程可以使用单调栈解决。
由此,子问题得到解决。
合并子问题:设一共有 c c c段,电线杆 i i i所在的段的编号为 g i g_i gi。
时间 t t t的答案为 s 1 × s 2 × ⋯ × s g t − 1 × Z × s g t + 1 × ⋯ × s c s_1\times s_2 \times \dots \times s_{g_{t}-1}\times Z \times s_{g_{t}+1}\times\dots\times s_c s1×s2×⋯×sgt−1×Z×sgt+1×⋯×sc。
其中 Z Z Z为子问题 g t g_t gt中最后倒下的电线杆是 t t t的概率, s i s_i si为子问题 i i i中最后倒下的电线杆编号小于 t t t的概率之和。
可以发现这是一个单点修改,区间查询问题,考虑使用线段树优化。
线段树中维护 s s s,每次将答案算出后,将 Z Z Z加到 s g t s_{g_t} sgt中。
注意:题目要求要将答案乘上 2 N 2^N 2N,但解决子问题时不能乘 2 N 2^N 2N,而要乘 2 l 2^l 2l,这样所有子问题乘起来才是 2 N 2^N 2N。
代码:
#include<bits/stdc++.h>
typedef long long LL;
using namespace std;
const LL N = 2e5 + 5;
const LL mod = 998244353;
struct segt {
struct node {
LL l, r, v;
} t[N << 2];
#define ls (p << 1)
#define rs (p << 1 | 1)
void build(LL p, LL l, LL r) {
t[p].l = l;
t[p].r = r;
t[p].v = 0;
if (l == r)
return;
LL mid = (l + r) >> 1;
build(ls, l, mid);
build(rs, mid + 1, r);
}
void add(LL p, LL id, LL v) {
if (t[p].l == t[p].r) {
t[p].v += v;
t[p].v %= mod;
return;
}
if (id <= t[ls].r)
add(ls, id, v);
else
add(rs, id, v);
t[p].v = t[ls].v * t[rs].v, t[p].v %= mod;
}
LL query(LL p, LL l, LL r) {
if (l <= t[p].l && t[p].r <= r)
return t[p].v;
LL res = 1;
if (t[ls].r >= l) {
res *= query(ls, l, r);
res %= mod;
}
if (t[rs].l <= r) {
res *= query(rs, l, r);
res %= mod;
}
return res;
}
} T;
struct Point {
LL x, y;
};
bool cmp(Point a, Point b) { return a.x < b.x; }
LL n, h, c, x[N], g[N], t[N], k[N], pow2[N];
Point a[N];
vector<LL> p[N];
vector<LL> res[N];
void solve(LL id) {
LL m = p[id].size() - 1;
stack<LL> stk;
for (LL i = 1; i <= m; i++) {
while (!stk.empty() && p[id][i] < stk.top())
stk.pop();
stk.push(p[id][i]);
k[i] = stk.size() - 1;
}
stack<LL> sstk;
for (LL i = m; i >= 1; i--) {
while (!sstk.empty() && p[id][i] < sstk.top())
sstk.pop();
sstk.push(p[id][i]);
k[i] += sstk.size() - 1;
}
res[id].emplace_back(0);
for (LL i = 1; i <= m; i++) {
LL b = (i == 1 || p[id][i - 1] < p[id][i]) + (i == m || p[id][i] > p[id][i + 1]);
res[id].emplace_back(b * pow2[m - k[i] - 1] % mod);
}
}
int main() {
cin >> n >> h;
pow2[0] = 1;
for (LL i = 1; i <= n; i++) {
cin >> x[i];
a[i].x = x[i];
a[i].y = i;
pow2[i] = (pow2[i - 1] << 1) % mod;
}
for (LL i = 1; i <= n; i++)
p[i].emplace_back(0);
sort(a + 1, a + n + 1, cmp);
g[a[1].y] = ++c,
p[c].emplace_back(a[1].y),
t[a[1].y] = p[c].size() - 1;
for (LL i = 2; i <= n; i++) {
if (a[i].x - a[i - 1].x <= h) {
g[a[i].y] = c;
p[c].emplace_back(a[i].y);
t[a[i].y] = p[c].size() - 1;
} else {
g[a[i].y] = ++c;
p[c].emplace_back(a[i].y);
t[a[i].y] = p[c].size() - 1;
}
}
for (LL i = 1; i <= c; i++)
solve(i);
T.build(1, 1, c);
for (LL i = 1; i <= n; i++) {
LL x = res[g[i]][t[i]];
LL ans = 1;
if (g[i] - 1)
ans *= T.query(1, 1, g[i] - 1);
if (g[i] + 1 <= c) {
ans *= T.query(1, g[i] + 1, c);
ans %= mod;
}
ans *= x;
ans %= mod;
T.add(1, g[i], x);
cout << ans << ' ';
}
return 0;
}
赛后交流
在比赛结束后,会在交流群中给出比赛题解,同学们可以在赛后查看题解进行补题。
群号: 704572101,赛后大家可以一起交流做题思路,分享做题技巧,欢迎大家的加入。