挂分宝典
SB错误
输出时的取模
字符串(catalan数):
\(lxhgww\)最近接到了一个生成字符串的任务,任务需要他把\(n\)个\(1\)和\(m\)个\(0\)组成字符串,但是任务还要求在组成的字符串中,在任意的前\(k\)个字符中,\(1\)的个数不能少于\(0\)的个数。现在\(lxhgww\)想要知道满足要求的字符串共有多少个,聪明的程序员们,你们能帮助他吗?
输出数据是一行,包括1个数字,表示满足要求的字符串数目,这个数可能会很大,只需输出这个数除以\(20100403\)的余数
错误的打开:
printf("%lld\n", C(n + m, n) - C(n + m, n + 1));
正确的打开:
printf("%lld\n", (C(n + m, n) - C(n + m, n + 1) + mod) % mod);
挂分:30分
中间变量的类型
甜圈:
错误的打开:
void Pushdown(int u) {
int mul = t[u].mul, add = t[u].add;
if (mul != 1 && add) {
t[(u<<1)].sum *= mul;
t[(u<<1)].mul *= mul;
t[(u<<1)].add *= mul;
t[(u<<1|1)].sum *= mul;
t[(u<<1|1)].mul *= mul;
t[(u<<1|1)].add *= mul;
t[(u<<1)].sum += add;
t[(u<<1)].add += add;
t[(u<<1|1)].sum += add;
t[(u<<1|1)].add += add;
t[u].add = 0;
t[u].mul = 1;
}
}
正确的打开:
void Pushdown(int u) {
unsigned long long mul = t[u].mul, add = t[u].add;
if (mul != 1 && add) {
t[(u<<1)].sum *= mul;
t[(u<<1)].mul *= mul;
t[(u<<1)].add *= mul;
t[(u<<1|1)].sum *= mul;
t[(u<<1|1)].mul *= mul;
t[(u<<1|1)].add *= mul;
t[(u<<1)].sum += add;
t[(u<<1)].add += add;
t[(u<<1|1)].sum += add;
t[(u<<1|1)].add += add;
t[u].add = 0;
t[u].mul = 1;
}
}
挂分: < 90分(<是因为代码别的地方也有锅)
分块时数组记得多开至少一个块
不然就会莫名奇妙地越界???反正调了一下午就因为这个。
Upd:发现是个人实现方法的问题,正常的写法没问题...
匈牙利一定要用时间戳!!!
memset什么的不要
一定要开longlong !!!!!
一年OI一场空,不开longlong见祖宗。
多重判断时,一定考虑加不加else!!!
错误的打开方式:
if (pre && a[now]) tot++, t++, pre = 1;
if (!pre && !a[now]) tot++, t++, pre = 1;
if (pre && !a[now]) tot++, pre = 0;
if (!pre && a[now]) tot++, pre = 0;
正确的打开方式:
if (pre && a[now]) tot++, t++, pre = 1;
else if (!pre && !a[now]) tot++, t++, pre = 1;
else if (pre && !a[now]) tot++, pre = 0;
else if (!pre && a[now]) tot++, pre = 0;
断环成链时,数组一定开两倍!!!
数组该清空的时候就清空,不要蜜汁自信。
FHQ_Treap pushup记得左+右+1
积累本
小凯的疑惑
对于互质的两个数\(a, \, b\),其不能组成的最大的数为\(a \times b - a - b\)
证明:https://www.luogu.com.cn/blog/user13091/solution-p3951
另一个用到这个结论的题是小奇挖矿2
菜肴制作
建反图,大根堆跑拓扑,可让编号小的尽量靠前。
让编号小的尽量靠前不等于字典序最小。
另一个有关的题是:Wide Swap
「HEOI2012」采花、HH的项链
暴力是莫队。
正解是将询问按r排序,根据题意维护每个颜色的最近出现位置。
采花是维护每个颜色最近两个出现位置,HH的项链则是只维护最近一个,查询用树状数组实现(树状数组下标为0直接特判掉)。
读入时维护:
for (int i = 1; i <= n; ++i) {
col[i] = read();
last[i] = colst[col[i]];
colst[col[i]] = i;
}
回答询问:
for (int pos = 1, i = 1; pos <= n && i <= m; ++pos) {
Add(last[pos], 1);
Add(last[last[pos]], -1);
for (; q[i].r == pos && i <= m; ++i) ans[q[i].id] = Ask(pos) - Ask(q[i].l - 1);
}
军训队列
考虑先从小到大排序,结果一定不劣。
定义\(dp[i][j]\)为前\(i\)个,分\(j\)组的最小答案。
然后转移变成了:
因为后面的多项式既跟\(i\)有关又跟\(j\)有关,不能直接单调队列。
然后考虑到\(a[i]\)单调递增,故可以斜率优化之。
Simple
对于一个\(c= x \times n + y \times m\)来说,一定有\(gcd(n,m)|c\)
其实也就是\(c\)一定可以表示为\(x \times gcd(n,m) + y \times gcd(n,m)\)
所以让式子两边都除以\(gcd(n,m)\),对答案是没有影响的。
为什么这样做呢,因为除完之后,\(\frac{n}{gcd(n,m)}\)和\(\frac{m}{gcd(n,m)}\)就是互质的了。
所以就可以用上上面凯爹的诱惑的结论。
ll ans = 0;
ll d = Gcd(n, m), qd = q / d;
if (n > m) swap(n, m);
n /= d, m /= d;
if (qd >= n * m) ans += (qd - n * m + 1);
ll k = min(qd, n * m - 1);
for (ll i = 0; i * m <= k; ++i) {
ll x = k - i * m;
ans += x / n + (i != 0);
}
printf("%lld\n", q - ans);
元素周期表
CV自粉兔
建立一张二分图,左边的点代表 \(n\) 个周期,右边的点代表 \(m\) 个主族。
把每一个元素 \((x,y)\) 看作一条边,连接第 \(x\) 周期和第 \(y\) 主族。
那么我们的目标是是这个二分图变成完全二分图,也就是有 \(n \times m\) 条边。
考虑核聚变的条件:
\((r_1, c_1) + (r_1, c_2) + (r_2, c_1) \to (r_2, c_2)\)。
可以发现这个过程是不改变二分图中的连通分量的个数的。
而反过来,对于二分图中的某一个连通分量,也可以通过核聚变的方式,把这个连通分量变成“完全”的,也就是连接左右两部分的所有边都存在。
那么答案就是将这个二分图添加尽量少的边使得它联通的边数。
也就是: \(\text{连通分量的个数}-1\)。
然后,也可以用并查集实现维护联通块的个数。
Init();
for (int i = 1; i <= q; ++i) {
scanf("%d %d", &r[i], &c[i]);
Merge(r[i], c[i] + n);
}
int tot = 0;
for (int i = 1; i <= n + m; ++i) {
if (fa[i] == i) tot++;
}
printf("%d\n", tot - 1);
建设城市
小奇正在玩一款名为《魔方王国》的游戏。在游戏中它需要建设\(n\)座不同的城市。
小奇有\(m\)个相同的建设队,它需要将这些建设队分配到每个城市中。每个城市至少要分配1个建设队,至多分配\(k\)个建设队。当然,每个城市分配的建设队数量必须是整数。
你需要求出有多少种不同的方案。两个方案被视为不同的,当且仅当存在至少一个城市分配到的建设队数量不同。
\(n \leq 10^9\),\(m,k \leq 10^7\)
简单的题干,不简单的题...
首先,将\(m\)个物品分成\(n\)组,每组都不为空,方案数为\(m-1 \choose n-1\),这也就是高考数学里常用的隔板法...
连这都不知道数学能上100分? ——\(sir\)
像我一样数学菜到上不了100分的各位请移步大佬博客
然后隔板法已经保证了每组都不为空,所以我们需要对上界做出限制。
然后至少有\(i\)组超过\(k\)个的方案数为\({n \choose i} \times {{m - 1 - k \times i} \choose {n - 1}}\)
应该挺好理解的,但是发现\({m-1 \choose n-1} - {n \choose 1} \times {{m - 1 - k \times 1} \choose {n - 1}}\)并不对。
因为后面这个东西减多了。
举个例子:在\(n \choose 1\)中,我们选中了1组,并分给了1组\(k\)个物品,然后再隔板法随机分,假如又分给了1组2个物品,分给了2组\(k+1\)个物品。然后会发现假如在\(n \choose 1\)中选中了2组,分给了2组\(k\)个物品,又在隔板法中分给了2组1个物品,1组\(k+2\)个物品。假设其他组的分配方案两次都是一样的。
显然1组有\(k+2\)个物品,2组有\(k+1\)个物品这种情况我们算成了两次,但是由于每组最终分配到的数量是一样的,所以这只能算一种情况而不是两种。
怎么解决呢。
(不)容易发现,有两组超标的情况会多算1次,有三组超标的情况会多算2次...(这一块题解上并没写,所以感觉写得心里好没底)
然后就可以容斥搞他了。显然最后的答案是随便选-至少一个不满足的+至少两个不满足的-至少三个不满足的......
剩下的就是\(n\)很大,所以用排列数乘上阶乘逆元求组合数,跟那一天她与我许下约定是一样的。
ll ans = 0;
for (int i = 0; i <= n; ++i) {
if (m - 1 - k * i < 0) break;
if (i & 1) {
ans = (ans - A[i] * inv[i] % mod * C(m - 1 - k * i, n - 1) % mod + mod) % mod;
} else {
ans = (ans + A[i] * inv[i] % mod * C(m - 1 - k * i, n - 1) % mod) % mod;
}
}
printf("%lld", ans);
过河
因为每个青蛙都是等价的,所以能过一个是一个,这样的贪心是没问题的。
在贪心正确的基础上,发现限制跳过去的青蛙数量的瓶颈是:从一个石头(包括岸边)一步能跳到的石子数的最小值。
然后滑动窗口滑一遍就行了。
int ans = INT_MAX;
a[0] = 0;
a[n + 1] = L;
int l = 0, r = 1;
for (; r <= n + 1; ++l) {
while (a[r] - a[l] <= D && r <= n + 1) r++;
//printf("l: %d r: %d len: %d\n", l, r, r - l - 1);
ans = min(ans, r - l - 1);
}
if (ans >= m) printf("Excited\n");
else printf("%d\n", ans);
「JLOI2013」卡牌游戏
概率版约瑟夫游戏。
无论是概率DP,还是约瑟夫游戏,其实都是倒着做比正着做好做。这题也是一样。
定义\(dp[i][j]\)为还剩\(i\)个人时,\(j\)号获胜的概率。
显然的\(dp[1][1]=1\)
转移见代码:
dp[1][1] = 1.0;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
int pos;
if (a[j] % i) pos = a[j] % i;
else pos = i;
for (int k = 1; k < i; ++k) {
pos++;
if (pos > i) pos -= i;
dp[i][pos] += dp[i - 1][k] / (double) m;
}
}
}
for (int i = 1; i <= n; ++i) printf("%.2lf%% ", dp[n][i] * 100.0);
「TJOI2017」可乐
一眼就是矩阵优化DP。
问题是自爆怎么处理,也挺套路的,设一个虚点表示自爆即可,每个点都向这个虚点连边。
一定要注意这个虚点也要自己跟自己连边!!!
不然状态是传不过来的。把自爆理解为到了自爆点之后只能留在原地,不能到别处就行了。
两个矩阵的初始化:
for (int i = 1; i <= m; ++i) {
scanf("%d %d", &u, &v);
base[u][v] = 1;
base[v][u] = 1;
}
for (int i = 1; i <= n + 1; ++i) { //注意虚点也要连边
base[i][n + 1] = 1;
base[i][i] = 1;
}
ans[1][1] = 1;
快速幂: (卡常小技巧:矩阵乘时尽量把取模放外面,尤其是这样模数特别小的时候,直接1.98s变430ms)
while (t) {
if (t & 1) {
memset(tmp, 0, sizeof tmp);
for (int i = 1; i <= n + 1; ++i) {
for (int j = 1; j <= n + 1; ++j) {
for (int k = 1; k <= n + 1; ++k) {
tmp[i][j] = (tmp[i][j] + ans[i][k] * base[k][j]);
}
tmp[i][j] %= mod; //把取模放外面
}
}
for (int i = 1; i <= n + 1; ++i)
for (int j = 1; j <= n + 1; ++j)
ans[i][j] = tmp[i][j];
}
memset(tmp, 0, sizeof tmp);
for (int i = 1; i <= n + 1; ++i) {
for (int j = 1; j <= n + 1; ++j) {
for (int k = 1; k <= n + 1; ++k) {
tmp[i][j] = (tmp[i][j] + base[i][k] * base[k][j]);
}
tmp[i][j] %= mod;
}
}
for (int i = 1; i <= n + 1; ++i)
for (int j = 1; j <= n + 1; ++j)
base[i][j] = tmp[i][j];
t >>= 1;
}
另一个很好的矩阵优化DP的题是「NOI2020」美食家,虽然做过但是让我再做一遍我感觉还是做不出来...
「NOIP2016」换教室
考虑定义\(dp[i][j][0/1]\)为到第\(i\)个时间段,已经使用了\(j\)次申请机会,该点是否尝试申请。
而不能定义成:第\(i\)个时间段,已经使用了\(j\)次申请机会,该点在哪个教室。
因为申请教室不一定成功,暴力算到状态里会多出很多不必要且麻烦的计算。
而且发现我们只考虑\(i\)和\(i-1\),所有可能的转移情况并不多。
转移式子实在太长了一行放不下,所以分行写了一下,可能有点奇怪。
dp[1][0][0] = 0;
dp[1][1][1] = 0;
for (int i = 2; i <= ti; ++i) {
dp[i][0][0] = dp[i - 1][0][0] + dist[pos[0][i - 1]][pos[0][i]];
for (int j = 1; j <= min(haha, i); ++j) {
int C1 = pos[0][i - 1], C2 = pos[1][i - 1], C3 = pos[0][i], C4 = pos[1][i];
dp[i][j][0] = min(dp[i][j][0],
min(dp[i - 1][j][0] + dist[C1][C3],
dp[i - 1][j][1] + dist[C1][C3] * (1.0 - k[i - 1]) +
dist[C2][C3] * k[i - 1]));
dp[i][j][1] = min(dp[i][j][1],
min(dp[i - 1][j - 1][0] + dist[C1][C4] * k[i] + dist[C1][C3] * (1 - k[i]),
dp[i - 1][j - 1][1] +
dist[C2][C4] * k[i] * k[i - 1] +
dist[C2][C3] * k[i - 1] * (1 - k[i]) +
dist[C1][C4] * (1 - k[i - 1]) * k[i] +
dist[C1][C3] * (1 - k[i - 1]) * (1 - k[i])));
}
}
求和
对于一些有取模而且没有逆元的除法,可以考虑在取模之前先把除数给除掉,尤其适用于除数较小的情况。
另外,快速乘是每次×2,别再写挂了...
ll Qmul(ll x, ll t) {
x %= mod;
ll s = 0;
while (t) {
if (t & 1) s = (s + x) % mod;
x = x * 2 % mod;
t >>= 1;
}
return s;
}
ll Calc(ll x, ll y) {
ll tmp = x + y;
if (x % 2 == 0) x /= 2;
else if (y % 2 == 0) y /= 2;
else tmp /= 2;
return Qmul(Qmul(x, y), tmp);
}
简单计算
考虑所求答案:
与下面这个好求的式子的差别:
显然答案里的每一项因为直接下取整会比上面的每一项少一点贡献。
考虑计算偏差的贡献,可以找规律发现偏差的贡献一定是若干段以\(0\)为首项,以\(gcd(p,q)\)为公差,以\(p-gcd(p,q)\)为末项的等差数列。
所以总的偏差为:
最终化简的答案为:
分组配对
亲 学 长:
这个第二题我好像提到过
用倍增来代替二分 降低复杂度
感觉不太彳亍啊
学到了学到了,先用倍增缩小答案的取值范围,然后再二分。
物理课
用pi时,不要傻乎乎地手打 3.1415926535898 ...double会自动卡精的...用 acos(-1) 更香。
「CSP-S 2019」括号树(括号序列)
ans[i]记录到第i个点的答案。
cnt[i]表示从当前匹配上的括号向左有几个连续的匹配好的同级括号。
last[i]记录第i个位置上一个未匹配的左括号在哪。
考虑一个新匹配好的括号的贡献,一定是他自己加上他向左连续延伸的同级括号个数。
序列版(链):
for (int i = 1; i <= n; ++i) {
last[i] = last[i - 1];
ans[i] = ans[i - 1];
if (s[i] == '(') last[i] = i, cnt[i] = 0;
else if (s[i] == ')' && last[i]) {
ans[i] += 1 + cnt[last[i] - 1];
cnt[i] = cnt[last[i] - 1] + 1;
last[i] = last[last[i] - 1];
}
}
挪到树上:
for (int i = 1; i <= n; ++i) {
last[i] = last[fa[i]];
ans[i] = ans[fa[i]];
if (ch[i] == '(') {
last[i] = i;
} else if (ch[i] == ')' && last[i]) {
cnt[i] = cnt[fa[last[i]]] + 1;
last[i] = last[fa[last[i]]];
ans[i] += cnt[i];
}
finalans ^= (i * ans[i]);
}
最小距离
求每个特殊点到最近的特殊点的距离。
考虑多源最短路,记录每个点是被哪个特殊点更新的。
然后可以枚举每条边,如果一条边的两端是被同一个特殊点染色的,那么这条边一定不会对答案有贡献。
否则就更新两端的答案即可。
多源最短路(就是最开始入队多个点):
void Dij() {
memset(dis, 0x3f, sizeof dis);
memset(vis, 0, sizeof vis);
for (int i = 1; i <= qq; i++) {
dis[id[i]] = 0;
col[id[i]] = id[i];
q.push(NODE(id[i],0));
}
while (!q.empty()) {
int u = q.top().id;
q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (int i = head[u]; i; i = e[i].nex) {
int v = e[i].to;
if (dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
col[v] = col[u];
q.push(NODE(v, dis[v]));
}
}
}
}
更新答案:
for (int i = 1; i <= m; i++) {
u = rw[i].from, v = rw[i].to, w = rw[i].w;
if (col[u] == col[v] || !col[u] || !col[v]) continue;
ans[col[u]] = min(ans[col[u]], dis[u] + dis[v] + w);
ans[col[v]] = min(ans[col[v]], dis[u] + dis[v] + w);
}
真相
显然一个说这句话的人会把一条递推链断开。
所以我们从每个说这句话的人反向推回去,即可得到这个人说假话时和说真话时各自的贡献(即该块内说真话的人的数量),贡献差加到对应的桶里。
然后枚举每个桶,用一个初始的答案加上桶里的答案差,检查是否匹配即可。
反推时细节有点小恶心(一堆if然后没打else调了半天)。
while (T--) {
memset(buc, 0, sizeof buc);
id = 0;
n = read();
char op[4];
for (int i = 1; i <= n; ++i) {
scanf("%s", op);
if (op[0] == '+') {
a[i] = 1;
}
else if (op[0] == '-') {
a[i] = 0;
}
else {
a[i] = -1;
pos[++id] = i;
k[id] = read();
}
}
int startans = 0;
for (int i = 1; i <= id; ++i) {
int now = pos[i] - 1;
int t = 1;
int pre = 1;
int tot = 1;
if (!now) now = n;
while (a[now] != -1) {
if (pre && a[now]) tot++, t++, pre = 1;
else if (!pre && !a[now]) tot++, t++, pre = 1;
else if (pre && !a[now]) tot++, pre = 0;
else if (!pre && a[now]) tot++, pre = 0;
now = now - 1;
if (!now) now = n;
}
startans += tot - t;
buc[k[i]] += 2 * t - tot;
}
if (!id) {
int t;
if (a[1]) t = 1;
else if (!a[1]) t = 0;
int now = 2;
if (now == n + 1) now = 1;
while (now != 1) {
if (t && a[now]) t = 1;
else if (t && !a[now]) t = 0;
else if (!t && a[now]) t = 0;
else if (!t && !a[now]) t = 1;
if (now == n) break;
now = now + 1;
}
if (t) printf("consistent\n");
else printf("inconsistent\n");
continue;
}
int flag = 0;
for (int i = 0; i <= n; ++i) {
int ans = startans + buc[i];
if (ans == i) {
flag = 1;
break;
}
}
if (flag) printf("consistent\n");
else printf("inconsistent\n");
}
「AtCoder ARC107B」Quadruple
题意:给定\(N,K\),求满足以下两个条件的可重排列\((a,b,c,d)\)的个数。
- \(1 \leq a,b,c,d \leq N\)
- \(a+b-c-d=K\)
#include <cstdio>
#include <iostream>
long long N, K;
int main() {
std:: cin >> N >> K;
if (K < 0) K = -K;
K = 2 * N - K;
long long now = 2 * N, ans = 0;
while (K >= 2) {
ans += (now > N + 1 ? (now - 1 - 2 * (now - N - 1)) : now - 1) * (K > N + 1 ? (K - 1 - 2 * (K - N - 1)) : K - 1);
K--, now--;
}
std:: cout << ans << '\n';
return 0;
}
奇技淫巧
-
当你不知道用哪一种贪心对的时候,那就全用。
-
概率不会不要慌,\(-nan\)和\(0\)来帮忙。