[CSP-S2 2019]Emiya 家今天的饭 题解
题目地址:洛谷:P5664 Emiya 家今天的饭 – 洛谷 | 计算机科学教 …
May all the beauty be blessed.
比赛地址:Dashboard – Codeforces Round #493 (Div. 2) – Codeforces
官方题解:Codeforces Round #493 — Editorial – Codeforces
有$n$袋气球,每袋分别有$a_i$个气球,有两个人,每一袋气球都必须给一个人,且两个人都必须分到气球。求一种分配方案,使得两个人拥有的气球数不一样。无解输出$-1$。
数据范围:$1 \leq n \leq 10, 1 \leq a_i \leq 1000$
显然$n=1$无解,且$n=2, a_1=a_2$也无解。对于其他情况,我们枚举子集搞一搞就好了。复杂度$O(2^n)$。
打的时候写了个只分配一个人一袋其他的给另外一个人的情况,没找到反例,也没fst。复杂度$O(n)$。
// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
typedef long long LL;
inline char fgc() {
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1; register char c = fgc();
for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
return res * neg;
}
const int MAXN = 15;
int n, a[MAXN], sum;
int main() {
n = readint();
for(int i = 1; i <= n; i++) {
a[i] = readint();
sum += a[i];
}
if(n == 1) {
puts("-1"); return 0;
}
if(n == 2 && a[1] == a[2]) {
puts("-1"); return 0;
}
for(int i = 1; i <= n; i++) {
if(sum - a[i] != a[i]) {
printf("1\n%d", i);
return 0;
}
}
puts("-1");
return 0;
}
给你一个$n$个数的序列$a_i$,要求切若干刀把序列分成若干段,且每一段中奇数和偶数的个数相等。在$x$和$y$两个数之前切一刀的代价是$|x-y|$,要求代价不超过$B$的情况下,最多能切多少刀。
数据范围:$2 \leq n \leq 100, 1 \leq B \leq 100, 1 \leq a_i \leq 100$
首先显然我们可以预处理一个前缀和,$sum[0][i]$表示$a_1$到$a_i$之间有多少偶数,$sum[1][i]$统计奇数。接下来用一个动态规划解决这个问题,令$dp[i][j]$表示在$a_i$和$a_{i+1}$之前切一刀,且切完以后代价限制还剩余$j$的时候最多能切多少刀,可以在前面枚举一个$k$位置,做以下转移
$$ dp[i][j] = \max_{1 \leq k < i} \{ dp[k][j+|a_i-a_{i+1}|]+1 \} \ (sum[0][k] = sum[1][k])$$
而答案可以在所有合法的DP状态中寻找,即
$$ ans = \max_{1 \leq i < n, 0 \leq j \leq B} \{ dp[i][j] \} \ (sum[0][i]=sum[1][i], sum[0][n]-sum[0][i]=sum[1][n]-sum[1][i])$$
直接暴力做转移和统计答案即可,复杂度$O(nB)$。
// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
typedef long long LL;
inline char fgc() {
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1; register char c = fgc();
for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
return res * neg;
}
const int MAXN = 105;
int n, B, a[MAXN], dp[MAXN][MAXN], sum[2][MAXN], ans;
int main() {
n = readint(); B = readint();
for(int i = 1; i <= n; i++) {
a[i] = readint();
sum[0][i] += sum[0][i - 1];
sum[1][i] += sum[1][i - 1];
sum[a[i] & 1][i]++;
}
for(int i = 1; i < n; i++) { // cut at [i, i + 1]
int cost = std::abs(a[i] - a[i + 1]);
for(int j = 0; j <= B - cost; j++) {
for(int k = 0; k < i; k++) {
if(sum[0][i] - sum[0][k] == sum[1][i] - sum[1][k] && sum[0][k] == sum[1][k]) {
dp[i][j] = std::max(dp[i][j], dp[k][j + cost] + 1);
}
if(sum[0][n] - sum[0][i] == sum[1][n] - sum[1][i]) {
ans = std::max(ans, dp[i][j]);
}
}
}
}
printf("%d", ans);
return 0;
}
给你一个长度为$n$的01串,你可以对这个串做两种操作:
求把这个串变为全1串的最小代价。
数据范围:$1 \leq n \leq 300 000, 0 \leq x, y \leq 10^9$
可以观察到我们一定有两种可能的最优方案,如果原串中包含$a$段连续的全0段,一种是通过$a-1$次翻转把0的部分翻转到一起,再进行$1$次取反;另一种是直接原地进行$a$次取反。除此以外,没有更优方案。因此,我们只需要讨论$x$和$y$的大小关系,就可以$O(1)$地求出答案,除了$a=0$的情况无需花费任何代价。
// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
typedef long long LL;
inline char fgc() {
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1; register char c = fgc();
for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
return res * neg;
}
const int MAXN = 300005;
int n;
LL x, y, ans;
char s[MAXN];
int main() {
scanf("%d%lld%lld%s", &n, &x, &y, s + 1);
s[0] = '1';
int cnt = 0;
for(int i = 1; i <= n; i++) {
if(s[i - 1] != '0' && s[i] == '0') {
cnt++;
}
}
if(!cnt) {
puts("0"); return 0;
}
if(x < y) ans = (cnt - 1) * x + y;
else ans = cnt * y;
printf("%lld", ans);
return 0;
}
求可以用满足$x+y+p+q=n$的$x, y, p, q$以$x+5y+10p+50q$形式表示的正整数$a$的数量。
数据范围:$1 \leq n \leq 10^9$
思路的表述基于原题面,题意简述有点简单过头了2333
我们考虑一个问题,既然要往$n$个格子里填I、V、X、L四种符号,我们不如把每个符号代表的值-1计算,就是填入的数字变为了$\{ 0, 4, 9, 49 \}$。
我们考虑只有$\{ 0, 4, 9 \}$的情况,显然,当填入了9个4的时候,可以用4个9去替换它,剩下的空位就填入0,这样就构造出了一种统计重复的情况。为了避免重复,我们可以从0到$\min(8, n)$枚举4的个数,剩下的位置填入0或9的所有方案数加和即是去过重后的方案数。
现在我们的集合中加入了49,问题变得复杂了一些。我们依然研究可能产生重复的情况,我们想要求这样的一些数对$(i, j)$,使得$4i+9j$表示出来的数加减若干个49后会与另外一个数对$(i’, j’)$表示的数重复。这样的情况,我们只需要统计$i+j$的最小值,在其他空位上填0或49,就可以不重不漏地统计了。实际上这个我们可以理解为在模49意义下做统计。显然,这里的$i, j < 49$。
对于每个模49后的值,维护得到这个值的最小$i+j$值$a_i$,答案就是$\sum_{i=0}^{48} (n-a_i+1)$。
复杂度$O(49^2)$。
诶等一下,打了个表发现$n=12$以后的答案是等差数列,公差49,我们得到了优秀的$O(1)$打表做法。
其实你对着上面的正统做法看一眼,就知道为什么公差是49了2333
// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
typedef long long LL;
inline char fgc() {
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1; register char c = fgc();
for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
return res * neg;
}
const int MAXN = 55;
int n, mn[MAXN];
int main() {
n = readint();
memset(mn, 0x3f, sizeof(mn));
for(int i = 0; i < 49; i++) {
for(int j = 0; j < 49; j++) {
mn[(4 * i + 9 * j) % 49] = std::min(mn[(4 * i + 9 * j) % 49], i + j);
}
}
LL ans = 0;
for(int i = 0; i < 49; i++) {
ans += std::max(0, n - mn[i] + 1);
}
printf("%lld", ans);
return 0;
}
给你一个$n \times n$的矩阵,每个格子有三种颜色可以填,求存在一行或者一列填入了同种颜色的矩阵填色方案数。输出答案对$998244353$取模后的值。
数据范围:$1 \leq n \leq 1 000 000$
如果我们已知$f(i, j)$表示有i行和j列被染上同种颜色的方案数,那么答案可以用经典容斥计算,即
$$ ans = \sum_{0 \leq i \leq n, 0 \leq j \leq n, i+j>0} \binom{n}{i} \binom{n}{j} (-1)^{i+j+1} f(i, j) $$
我们接下来讨论一下怎么求$f(i, j)$。
我们发现,当$i, j$中一个为0的时候,情况比较好办,例如,$f(0, k)=3^k \cdot 3^{n(n-k)}$,选中的$k$列每一列指定一种颜色,其他格子XJB填就好。
而更普遍的情况是$i, j$都不为0的情况,由于行列中间有交点,此时同种颜色的行列的颜色一定相同,即$f(i, j)=3 \cdot 3^{(n-i)(n-j)}$。
解决了$f(i, j)$的问题后,我们发现,直接暴力求值复杂度会是$O(n^2)$以上的,不好处理,我们需要优化。
对于$i, j$中一个为0的情况,我们不妨暴力统计,这部分的复杂度是$O(n \log n)$的。
另外一部分写出来是这样的
$$ \sum_{i=1}^n \sum_{j=1}^n \binom{n}{i} \binom{n}{j} (-1)^{i+j+1} 3 \cdot 3^{(n-i)(n-j)} $$
换个元先,$i \rightarrow n-i, j \rightarrow n-j$
$$ \sum_{i=0}^{n-1} \sum_{j=0}^{n-1} \binom{n}{n-i} \binom{n}{n-j} (-1)^{2n-i-j+1} 3^{ij+1} $$
我们知道组合数的对称性以及一些关于$-1$的幂次的性质,可以变形如下
$$ \sum_{i=0}^{n-1} \sum_{j=0}^{n-1} \binom{n}{i} \binom{n}{j} (-1)^{i+j+1} 3^{ij+1} $$
我们固定$i$来枚举$j$,改变求和顺序
$$ 3 \cdot \sum_{i=0}^{n-1} \binom{n}{i} (-1)^{i+1} \sum_{j=0}^{n-1} \binom{n}{j} (-1)^j 3^{ij} $$
把$-1$和$3$合并一下
$$ 3 \cdot \sum_{i=0}^{n-1} \binom{n}{i} (-1)^{i+1} \sum_{j=0}^{n-1} \binom{n}{j} (-3^i)^j $$
我们知道有二项式定理$(a+b)^n = \sum_{i=0}^n \binom{n}{i} a^i b^{n-i}$,后面对$j$的求和可以构造二项式定理的形式简化成求乘幂
$$ 3 \cdot \sum_{i=0}^{n-1} \binom{n}{i} (-1)^{i+1} \{ [1+(-3^i)]^n – (-3^i)^n \} $$
现在这个式子只需要枚举$i$就可以做求和了,复杂度$O(n \log n)$,只需要对上面两种情况分开求和就可以把复杂度搞成对的了。
// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
typedef long long LL;
inline char fgc() {
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1; register char c = fgc();
for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
for(; isdigit(c); c = fgc()) res = (res << 1) + (res << 3) + c - '0';
return res * neg;
}
const int MAXN = 1000005, MO = 998244353;
inline LL fpow(LL n, LL k) {
LL res = 1;
for(; k; k >>= 1) {
if(k & 1) res = res * n % MO;
n = n * n % MO;
}
return res;
}
LL mul[MAXN], inv[MAXN];
inline void calc() {
mul[0] = mul[1] = inv[0] = inv[1] = 1;
for(int i = 2; i < MAXN; i++) {
mul[i] = mul[i - 1] * i % MO;
}
for(int i = 2; i < MAXN; i++) {
inv[i] = (-(MO / i) * inv[MO % i] % MO + MO) % MO;
}
for(int i = 2; i < MAXN; i++) {
inv[i] = inv[i - 1] * inv[i] % MO;
}
}
inline LL C(int n, int m) {
return mul[n] * inv[m] % MO * inv[n - m] % MO;
}
LL n;
int main() {
calc();
n = readint();
LL ans = 0;
for(int i = 1, j = 1; i <= n; i++, j *= -1) {
ans = (ans + 2 * C(n, i) * j % MO * fpow(3, i + n * (n - i)) % MO + MO) % MO;
}
for(int i = 0, j = -1; i < n; i++, j *= -1) {
ans = (ans + 3 * C(n, i) * j % MO
* (fpow(1 - fpow(3, i), n) - fpow(-fpow(3, i), n)) % MO + MO) % MO;
}
ans = (ans % MO + MO) % MO;
printf("%lld", ans);
return 0;
}
题目地址:洛谷:【P3565】[POI2014]HOT-Hotels – 洛谷、BZOJ:Problem 3522. — [Poi2014]Hotel
有一个树形结构的宾馆,n个房间,n-1条无向边,每条边的长度相同,任意两个房间可以相互到达。吉丽要给他的三个妹子各开(一个)房(间)。三个妹子住的房间要互不相同(否则要打起来了),为了让吉丽满意,你需要让三个房间两两距离相同。
有多少种方案能让吉丽满意?
输入格式:
第一行一个数n。
接下来n-1行,每行两个数x,y,表示x和y之间有一条边相连。
输出格式:
让吉丽满意的方案数。
输入样例#1:
7 1 2 5 7 2 5 2 3 5 6 4 5
输出样例#1:
5
【样例解释】
{1,3,5},{2,4,6},{2,4,7},{2,6,7},{4,6,7}
【数据范围】
n≤5000
参考资料:题解 P3565 【[POI2014]HOT-Hotels】 – – 洛谷博客
很容易有一个思路就是枚举树根点拉成有根树,然后从根的不同子树中任取三个子树,从这些子树的同深度点集中每个子树任取一个,求这样的方案数之和即可。
问题是,从一堆集合中任取三个,再从这三个集合中任取一个元素这样的方案数总和是一个很难求的东西。接下来是一波神仙分析。
当只有3个集合,元素个数分别是a、b、c时,显然上面说的方案数就是abc。
当集合变为4个,新加入的集合元素个数为d时,我们会发现答案会增加d(ab+bc+ac)。
当集合变为5个,新加入的集合元素个数为e时,我们会发现答案会增加e(ab+bc+ac+ad+bd+cd)。
……
注意上面括号内的部分,实际上每一次会增加上一次元素个数×上一次之前的元素个数和这么多,我们把括号内的这个东西维护成sum2,其实际意义是任选2个集合再分别从两个集合中各任选1个的方案数,而维护sum1为所有集合的元素个数和。每次新加入一个集合就可以往答案中加入new×sum2,sum2加入new×sum1,sum1加入new,这样就很好维护了。
于是我们解决了这个数学模型的问题。接下来的就是对这一棵子树的每一个深度集合都做一遍这个操作即可。
总复杂度为O(n^2 \log n)。
// Code by KSkun, 2018/6
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <vector>
typedef long long LL;
inline char fgc() {
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1;
register char c = fgc();
while(!isdigit(c)) {
if(c == '-') neg = -1;
c = fgc();
}
while(isdigit(c)) {
res = (res << 1) + (res << 3) + c - '0';
c = fgc();
}
return res * neg;
}
const int MAXN = 5005;
int n;
std::vector<int> gra[MAXN];
LL ans, sum1[MAXN], sum2[MAXN];
int cnt[MAXN], mxdep;
inline void dfs(int u, int fa, int dep) {
cnt[dep]++; mxdep = std::max(mxdep, dep);
for(int i = 0; i < gra[u].size(); i++) {
int v = gra[u][i];
if(v == fa) continue;
dfs(v, u, dep + 1);
}
}
int main() {
n = readint();
for(int i = 1, a, b; i < n; i++) {
a = readint(); b = readint();
gra[a].push_back(b); gra[b].push_back(a);
}
for(int i = 1; i <= n; i++) {
memset(sum1, 0, sizeof(sum1));
memset(sum2, 0, sizeof(sum2));
for(int j = 0; j < gra[i].size(); j++) {
int v = gra[i][j];
mxdep = 0; dfs(v, i, 1);
for(int k = 1; k <= mxdep; k++) {
ans += cnt[k] * sum2[k];
sum2[k] += sum1[k] * cnt[k];
sum1[k] += cnt[k];
cnt[k] = 0;
}
}
}
printf("%lld", ans);
return 0;
}
题目地址:洛谷:【P2518】[HAOI2010]计数 – 洛谷、BZOJ:Problem 2425. — [HAOI2010]计数
你有一组非零数字(不一定唯一),你可以在其中插入任意个0,这样就可以产生无限个数。比如说给定{1,2},那么可以生成数字12,21,102,120,201,210,1002,1020,等等。
现在给定一个数,问在这个数之前有多少个数。(注意这个数不会有前导0).
输入格式:
只有1行,为1个整数n.
输出格式:
只有整数,表示N之前出现的数的个数。
输入样例#1:
1020
输出样例#1:
7
n的长度不超过50,答案不超过2^63-1.
参考资料:题解 P2518 【[HAOI2010]计数】 – noob – 洛谷博客
如果我们不删去前导0,其实就是相当于求对给定数的全排列中,比这个数小的排列个数。考虑一个数位DP的思想,如果一个高位上填的数字已经更小了,后面的位置显然是可以瞎邒排的,直接往答案里加一个全排列就好。但是可重集的全排列是有可能爆LL的,这里我们采用一种折中的办法求:如果集合大小为n,每个数码的个数为xi,考虑从n个位置里先取x0个位置放置0,然后从n-x0个位置放置1,,以此类推。也就是说,上面那个可重集的全排列数量是
\prod_{i=0}^9 \mathrm{C}_{n - \sum_{j=0}^{i-1} x_j}^{x_i}
我们枚举若前面的所有数位都与原数相同且现在这一位填某数码的时候计算答案,加进去就可以了。
// Code by KSkun, 2018/5
#include <cstdio>
#include <cctype>
#include <cstring>
typedef long long LL;
const int MAXN = 55;
LL C[MAXN][MAXN];
inline void calc() {
C[0][0] = 1;
for(int i = 1; i < MAXN; i++) {
C[i][0] = 1;
for(int j = 1; j <= i; j++) {
C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
}
}
}
char num[MAXN];
int n, cnt[10];
inline LL cal(int n, int *cnt) {
LL res = 1;
for(int i = 0; i <= 9; i++) {
res *= C[n][cnt[i]];
n -= cnt[i];
}
return res;
}
int main() {
calc();
scanf("%s", num + 1); n = strlen(num + 1);
for(int i = 1; i <= n; i++) {
cnt[num[i] - '0']++;
}
LL ans = 0;
for(int i = 1; i <= n; i++) {
for(int j = 0; j < num[i] - '0'; j++) {
if(cnt[j]) {
cnt[j]--;
ans += cal(n - i, cnt);
cnt[j]++;
}
}
cnt[num[i] - '0']--;
}
printf("%lld", ans);
return 0;
}