挂分宝典

SB错误

输出时的取模

字符串(catalan数):

(lxhgww)最近接到了一个生成字符串的任务,任务需要他把(n)(1)(m)(0)组成字符串,但是任务还要求在组成的字符串中,在任意的前(k)个字符中,(1)的个数不能少于(0)的个数。现在(lxhgww)想要知道满足要求的字符串共有多少个,聪明的程序员们,你们能帮助他吗?

输出数据是一行,包括1个数字,表示满足要求的字符串数目,这个数可能会很大,只需输出这个数除以(20100403)的余数

错误的打开:

printf("%lld
", C(n + m, n) - C(n + m, n + 1));

正确的打开:

printf("%lld
", (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 imes 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)组的最小答案。
然后转移变成了:

[dp[i][j]= min_{k=1}^{i}(dp[i - 1][j - 1] + (a[i] - a[k])^2) ]

因为后面的多项式既跟(i)有关又跟(j)有关,不能直接单调队列。
然后考虑到(a[i])单调递增,故可以斜率优化之。

Simple

对于一个(c= x imes n + y imes m)来说,一定有(gcd(n,m)|c)
其实也就是(c)一定可以表示为(x imes gcd(n,m) + y imes 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
", q - ans);

元素周期表

CV自粉兔

建立一张二分图,左边的点代表 (n) 个周期,右边的点代表 (m) 个主族。

把每一个元素 ((x,y)) 看作一条边,连接第 (x) 周期和第 (y) 主族。

那么我们的目标是是这个二分图变成完全二分图,也就是有 (n imes m) 条边。

考虑核聚变的条件:

((r_1, c_1) + (r_1, c_2) + (r_2, c_1) o (r_2, c_2))

可以发现这个过程是不改变二分图中的连通分量的个数的。

而反过来,对于二分图中的某一个连通分量,也可以通过核聚变的方式,把这个连通分量变成“完全”的,也就是连接左右两部分的所有边都存在。

那么答案就是将这个二分图添加尽量少的边使得它联通的边数。

也就是: ( ext{连通分量的个数}-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
", 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} imes {{m - 1 - k imes i} choose {n - 1}})

应该挺好理解的,但是发现({m-1 choose n-1} - {n choose 1} imes {{m - 1 - k imes 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
", l, r, r - l - 1);
    ans = min(ans, r - l - 1);
}
if (ans >= m) printf("Excited
");
else printf("%d
", 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);
}

简单计算

考虑所求答案:

[left lfloor frac{q}{p} ight floor + left lfloor frac{2q}{p} ight floor + left lfloor frac{3q}{p} ight floor + ...... + left lfloor frac{(p-1)q}{p} ight floor + left lfloor frac{pq}{p} ight floor ]

与下面这个好求的式子的差别:

[left lfloor frac{q}{p} + frac{2q}{p} + frac{3q}{p} + ...... + frac{(p - 1)q}{p} + frac{pq}{p} ight floor ]

显然答案里的每一项因为直接下取整会比上面的每一项少一点贡献。
考虑计算偏差的贡献,可以找规律发现偏差的贡献一定是若干段以(0)为首项,以(gcd(p,q))为公差,以(p-gcd(p,q))为末项的等差数列
所以总的偏差为:

[frac{frac{(p-gcd(p,q)) imes gcd(p,q)}{2}}{p} imes frac{p}{gcd(p,q)} = frac{p - gcd(p, q)}{2} ]

最终化简的答案为:

[left lfloor frac{frac{(q+pq) imes p}{2}}{p} ight floor - left lfloor frac{p - gcd(p, q)}{2} ight floor = left lfloor frac{q(p+1)}{2} ight floor - left lfloor frac{p - gcd(p, q)}{2} ight floor ]

分组配对

亲 学 长:

这个第二题我好像提到过

用倍增来代替二分 降低复杂度

感觉不太彳亍啊

学到了学到了,先用倍增缩小答案的取值范围,然后再二分。

物理课

用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
");
            else printf("inconsistent
");
            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
");
        else printf("inconsistent
");
    }

「AtCoder ARC107B」Quadruple

题意:给定(N,K),求满足以下两个条件的可重排列((a,b,c,d))的个数。

  1. (1 leq a,b,c,d leq N)
  2. (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 << '
';
	return 0;
}

奇技淫巧

  1. 当你不知道用哪一种贪心对的时候,那就全用。

  2. 概率不会不要慌,(-nan)(0)来帮忙。

好文收藏

一种基于错误的寻找重心方法的点分治的复杂度分析

「洛谷日报」FHQ_Treap

原文地址:https://www.cnblogs.com/Zfio/p/book.html