洛谷 CSP-J/S 2023 模拟赛题解
P9740 「KDOI-06-J」ION 比赛
难度:入门
设已有成绩为 Already Au.。
否则,对于第
- 若
s+\frac{100(a_i-b_i)}{a_i}<t ,输出NaN。 - 否则,输出
\lceil\frac{(t-s)a_i}{100}\rceil 。
#include <cstdio>
using namespace std;
int a[10], b[10];
inline int ceil(int x, int y) {
return x % y ? x / y + 1 : x / y;
}
int main() {
int n, au, pts = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d%d", &a[i], &b[i]),
pts += 100 / a[i] * b[i];
scanf("%d", &au);
if (pts >= au) {
puts("Already Au.");
return 0;
}
for (int i = 1; i <= n; i++) {
if (pts + 100 / a[i] * (a[i] - b[i]) < au)
puts("NaN");
else
printf("%d\n", ceil(au - pts, 100 / a[i]));
}
return 0;
}
P9741 「KDOI-06-J」翻转与反转
难度:普及-
找规律。第
具体而言,若
#include <cstdio>
using namespace std;
int a[2000005], b[2000005];
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
if (n - i + 1 & 1)
b[(n - i + 1) / 2 + 1] = a[i] ^ 1;
else
b[i + (n - i + 1) / 2] = a[i];
}
for (int i = 1; i <= n; i++)
printf("%d ", b[i]);
return 0;
}
P9742 「KDOI-06-J」贡献系统
难度:普及+/提高
贪心。首先把贡献为正的放在上面,贡献为负的放在下面,并保证贡献同号的相对位置不变。这样,上面有一段排名是没改变的,下面有一段也是。中间的部分排名都会改变,而且若是正数则排名一定上升,若是负数则排名一定下降。可以这么理解:把一个上面的负贡献抽了出来塞到下面,导致它前后位置之间的所有人全部向上一位;把一个下面的正贡献抽了出来塞到上面,导致它前后位置之间的所有人全部向下一位。又因为贡献同号的相对位置是不变的,导致我们抽出来再塞回去这个操作中,夹在中间改变排名的都是和被操作数异号的,因此中间的数我们已经实现了贡献最大化。
现在的问题是,上下各一段排名没有改变的人,如何使他们的贡献最大化。我们先考虑上面的正贡献人群。此时整一段里全部是正贡献。
引理 1:如果要对一个人进行操作,那么一定是把他往下面塞。
我们把一个人往下面塞,就是牺牲他自己的贡献,换取中间所有人的贡献,往上面塞就相反。有人就会问了,我们往上面塞,牺牲他上面的换他自己的,可能也会值得啊。是的,是会值得,但是还能更值得。我们想要让他正贡献,不需要牺牲那么多上面的,牺牲顶上一个就够了。而牺牲顶上一个,相当于把顶上那个往下面塞,所以又绕了回来,往下面塞一定不劣。
引理 2:如果要对一个人进行操作,那么一定是把他往下面塞到底。
把这个人往下塞,本来就是让他做活雷锋的,那既然做了活雷锋,就做到底嘛,让最多的人享受正贡献,反正不管他掉多少名,吃的负贡献都是不变的。明明可以让他下面的人全部吃到正贡献,为什么要在一半又塞回去呢?这样底下的人就吃不到正贡献了,血亏。
引理 3:最多只会对一个人进行操作。
如果有两个人,保留原排名高的那一个,把他塞到底,一定比两个人要更优,因为吃的正贡献是一样的,而且还少一个人的负贡献。
那么根据上面的结论,我们可以在整一段里面枚举要把哪个人塞到底,并计算把他塞到底时的总贡献。下面的负数段同理,枚举把哪一个人塞到顶。时间复杂度
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
struct man {
int rating, contrib, rank; // rank = rating rank
} a[200005];
inline bool comp(const man& a, const man& b) {
return a.contrib * b.contrib > 0 ? a.rank < b.rank : a.contrib > b.contrib;
}
signed main() {
int T;
scanf("%lld", &T);
while (T--) {
int n;
scanf("%lld", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i].rating);
a[i].rank = i;
}
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i].contrib);
sort(a + 1, a + n + 1, comp);
int st = 0, ed = n + 1;
while (st < n && a[st + 1].rank == st + 1 && a[st + 1].contrib > 0)
st++;
while (ed > 1 && a[ed - 1].rank == ed - 1 && a[ed - 1].contrib < 0)
ed--;
int ans = 0;
if (a[1].contrib > 0) {
int cost = 0, sum = 0;
for (int i = 1; i <= st; i++)
sum += a[i].contrib;
for (int i = 1; i <= st; i++) {
cost = max(cost, sum - a[i].contrib * 2);
sum -= a[i].contrib;
}
ans += cost;
}
if (a[n].contrib < 0) {
int cost = 0, sum = 0;
for (int i = n; i >= ed; i--)
sum -= a[i].contrib;
for (int i = n; i >= ed; i--) {
cost = max(cost, sum + a[i].contrib * 2);
sum += a[i].contrib;
}
ans += cost;
}
for (int i = st + 1; i < ed; i++)
if (i < a[i].rank)
ans += a[i].contrib;
else if (i > a[i].rank)
ans -= a[i].contrib;
printf("%lld\n", ans);
}
return 0;
}
P9743 「KDOI-06-J」旅行
难度:普及+/提高
从这里开始进入 dp 专场。这道题是完全背包在加上一些位置上的转移。
设
每次我们来到一个站点,状态都是尚未购买该站点的票的。于是我们在出站之前应该考虑买一买票,买法就是完全背包:
我觉得是不能把两种票一起买的,因为先 L 后 Z 和先 Z 后 L 会算两次,具体会不会寄我也没试,但是分开买肯定是对的。
然后买完票就上车走人:
时间复杂度是
#include <cstdio>
#define int long long
#define mod 998244353
using namespace std;
int dp[2][50][95][50][50];
int a[50][50], b[50][50];
signed main() {
int n, m, K;
scanf("%lld%lld%lld", &n, &m, &K);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%lld", &a[i][j]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%lld", &b[i][j]);
dp[1][1][K][0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
for (int k = 0; k <= K; k++)
for (int l = 0; l < n; l++)
for (int z = 0; z < m; z++)
dp[i & 1 ^ 1][j][k][l][z] = 0;
for (int j = 1; j <= m; j++) {
for (int k = K - a[i][j]; k >= 0; k--)
for (int l = 1; l < n; l++)
for (int z = 0; z < m; z++)
dp[i & 1][j][k][l][z] += dp[i & 1][j][k + a[i][j]][l - 1][z],
dp[i & 1][j][k][l][z] %= mod;
for (int k = K - b[i][j]; k >= 0; k--)
for (int l = 0; l < n; l++)
for (int z = 1; z < m; z++)
dp[i & 1][j][k][l][z] += dp[i & 1][j][k + b[i][j]][l][z - 1],
dp[i & 1][j][k][l][z] %= mod;
printf("%lld ", dp[i & 1][j][0][0][0] % mod);
if (i < n)
for (int k = 0; k <= K; k++)
for (int l = 1; l < n; l++)
for (int z = 0; z < m; z++)
dp[i & 1 ^ 1][j][k][l - 1][z] += dp[i & 1][j][k][l][z],
dp[i & 1 ^ 1][j][k][l - 1][z] %= mod;
if (j < m)
for (int k = 0; k <= K; k++)
for (int l = 0; l < n; l++)
for (int z = 1; z < m; z++)
dp[i & 1][j + 1][k][l][z - 1] += dp[i & 1][j][k][l][z],
dp[i & 1][j + 1][k][l][z - 1] %= mod;
}
puts("");
}
return 0;
}
P9744 「KDOI-06-S」消除序列
难度:普及+/提高
既然是 dp 专场,那么这题肯定是 dp 嘛。
首先想暴力。设
后面那个
这个时候我们就要运用一些贪心的思想。首先我们要从
就是说,要么仍然是一波操作 1,要么先改前面再改当前。然后,很容易发现操作 1 最多只会进行一次,因为前面的会被后面的完全覆盖。其实我们一开始写的 dp 式就是这个意思:要么在前面执行操作 1,要么在当前位执行操作 1,不管怎样,只会进行一次。对
对于每一个
综上所述,dp 式就是
两只
#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
int a[500005], b[500005], c[500005], p[500005], s[500005], g[500005], dp[500005];
signed main() {
int n;
scanf("%lld", &n);
for (int i = 1; i <= n; i++)
scanf("%lld", &a[i]);
for (int i = 1; i <= n; i++) {
scanf("%lld", &b[i]);
s[i] = s[i - 1] + b[i];
}
for (int i = 1; i <= n; i++)
scanf("%lld", &c[i]);
a[1] = min(a[1], b[1]);
for (int i = 1; i <= n; i++)
a[i] = min(a[i], a[i - 1] + b[i]);
a[n + 1] = a[n];
int q;
scanf("%lld", &q);
while (q--) {
int tot = 0;
scanf("%lld", &tot);
for (int i = 1; i <= tot; i++) {
scanf("%lld", &p[i]);
g[i] = g[i - 1] + c[p[i]];
}
p[++tot] = n + 1;
g[tot] = g[tot - 1];
for (int i = 1; i <= tot; i++)
dp[i] = min(dp[i - 1] + s[p[i] - 1] - s[p[i - 1]], a[p[i] - 1] + g[i - 1]);
printf("%lld\n", dp[tot]);
}
return 0;
}
P9745 「KDOI-06-S」树上异或
难度:提高+/省选-
既然是 dp 专场,那么这题肯定是 dp 嘛。而且是树上问题,所以是树形 dp。
但是这题的答案形式是非常的便秘,套了三层,连通块内的异或和,连通块之间乘积,方案之间加和,极其逆天。我们还是从简单的情形入手。
特殊性质 A:保证对于任意
1<i\le n ,f_i=i-1 。
那么这个时候“树”就变成了“链”,“连通块”也就变成了“序列上的连续区间”。设
后面那个
我们发现,每次都跳到区间的开头是一个非常弱智的想法,我们只需要考虑它和它的前一个点是不是接起来的,同时再维护一个当前区间的异或和,就是这样:
第一条式子就是和前一个接上,第二条就是和前一个断开,第二维就是
我们仔细观察一下第一条式子,发现第二维的操作就是异或,直接套路拆位,设
在新概念下,它应该为
那么原来的式子就可以写成:
这样就得到了一个
于是我们试图推广至树的情况。众所周知,树就是一条条链接起来。对于一个点
第一条是接,
因为在初始状态下,异或和就是
#include <cstdio>
#include <vector>
#define mod 998244353
using namespace std;
int dp[500005][65][2], g[500005];
long long x[500005];
vector<int> tr[500005];
void dfs(int u, int fa) {
for (int i = 0; i < 61; i++)
dp[u][i][(x[u] >> i) & 1] = 1;
for (int v: tr[u])
if (v != fa) {
dfs(v, u);
for (int i = 0; i < 61; i++) {
long long f0 = dp[u][i][0], f1 = dp[u][i][1];
dp[u][i][0] = (f0 * dp[v][i][0] + f1 * dp[v][i][1] + f0 * g[v]) % mod;
dp[u][i][1] = (f0 * dp[v][i][1] + f1 * dp[v][i][0] + f1 * g[v]) % mod;
}
}
for (int i = 0; i < 61; i++)
g[u] = (g[u] + (1ll << i) % mod * dp[u][i][1] % mod) % mod;
}
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%lld", &x[i]);
for (int i = 2; i <= n; i++) {
int f;
scanf("%d", &f);
tr[i].emplace_back(f);
tr[f].emplace_back(i);
}
dfs(1, 0);
printf("%d", g[1]);
return 0;
}
P9746 「KDOI-06-S」合并序列
难度:省选/NOI−
既然是 dp 专场,那么这题肯定是 dp 嘛。而且是区间问题,所以是区间 dp。
每次我们可以用三个数合并一个区间,但是这三个数也可以是由区间合并而来的,如果把它们展开来看,就是三个小区间合并一个大区间,而且这三个区间应该满足以下条件:
- 没有重叠,画成图就是下面这样(星号就是选中的三个区间)。为了方便叙述,我们称这三个区间的六个端点从左至右分别为
A 、B 、C 、D 、E 、F 。
----******-----*****--************----
A B C D E F
- 三个区间都是可以由更小的区间合并而来的。
- 三个区间内数字的总异或和为
0 。
那么我们可以设
这个东西前缀异或和随便做。那么很显然有一个
如果我们两个两个合并,时间复杂度就可以压至
我们首先定住
- 异或和等于
EF 这一段的异或和(一个定值); - 没有重叠,且都是可合成的;
- 左边那一段的左端点为
A ;
那么就可以合成
现在的问题就是如何求
现在要把两段区间合并起来,那么我们直接枚举另一端区间的异或和以及左端点。
里面那一层
那么就可以大大加快
那么我们就可以快速计算
输出方案的话直接 dfs 倒序输出。时间复杂度单次为
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
int a[550], f[550][550], g[550][550], h[550][550], dp[550][550], s[550];
int step(int A, int F) {
if (A == F)
return 0;
int B, C, D, E;
for (int i = F; i >= A + 2; i--)
if (dp[i][F] && h[A][s[i - 1] ^ s[F]] < i) {
D = h[A][s[i - 1] ^ s[F]];
E = i;
break;
}
for (int i = A; i < D; i++)
if (dp[A][i] && g[i + 1][s[E - 1] ^ s[F] ^ s[A - 1] ^ s[i]] == D) {
B = i;
break;
}
for (int i = B + 1; i <= D; i++)
if (dp[i][D] && f[i][s[E - 1] ^ s[F] ^ s[A - 1] ^ s[B]] == D) {
C = i;
break;
}
return step(A, B) + step(C, D) + step(E, F) + 1;
}
void print(int A, int F) {
if (A == F)
return;
int B, C, D, E;
for (int i = F; i >= A + 2; i--)
if (dp[i][F] && h[A][s[i - 1] ^ s[F]] < i) {
D = h[A][s[i - 1] ^ s[F]];
E = i;
break;
}
for (int i = A; i < D; i++)
if (dp[A][i] && g[i + 1][s[E - 1] ^ s[F] ^ s[A - 1] ^ s[i]] == D) {
B = i;
break;
}
for (int i = B + 1; i <= D; i++)
if (dp[i][D] && f[i][s[E - 1] ^ s[F] ^ s[A - 1] ^ s[B]] == D) {
C = i;
break;
}
print(E, F);
print(C, D);
print(A, B);
printf("%d %d %d\n", A, C - B + A, E - B + A - D + C);
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
memset(a, 0, sizeof(a));
memset(f, 63, sizeof(f));
memset(g, 63, sizeof(g));
memset(h, 63, sizeof(h));
memset(dp, 0, sizeof(dp));
memset(s, 0, sizeof(s));
int n;
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
dp[i][i] = 1;
s[i] = s[i - 1] ^ a[i];
}
for (int i = n; i >= 1; i--) {
f[i][a[i]] = min(f[i][a[i]], i);
for (int j = 0; j < 512; j++)
g[i][j] = min(f[i][j], g[i + 1][j]);
for (int j = 0; j < 512; j++)
if (f[i][j] < n)
for (int k = 0; k < 512; k++)
h[i][j ^ k] = min(h[i][j ^ k], g[f[i][j] + 1][k]);
for (int j = i + 2; j <= n; j++) {
for (int k = i + 2; k <= j; k++)
if (dp[k][j] && h[i][s[j] ^ s[k - 1]] < k) {
dp[i][j] = 1;
break;
}
if (!dp[i][j])
continue;
f[i][s[j] ^ s[i - 1]] = min(f[i][s[j] ^ s[i - 1]], j);
g[i][s[j] ^ s[i - 1]] = min(g[i][s[j] ^ s[i - 1]], j);
if (f[i][s[j] ^ s[i - 1]] < n)
for (int k = 0; k < 512; k++)
h[i][s[j] ^ s[i - 1] ^ k] = min(h[i][s[j] ^ s[i - 1] ^ k], g[f[i][s[j] ^ s[i - 1]] + 1][k]);
}
}
puts(dp[1][n] ? "Huoyu" : "Shuiniao");
if (dp[1][n]) {
printf("%d\n", step(1, n));
print(1, n);
}
}
return 0;
}
P9747 「KDOI-06-S」签到题
难度:省选/NOI-
咕咕咕……