省队集训Ⅱ-Day8
Day8
Color
这道题的优化是真的爽, 考场上的代码我存了 个版本, 每个版本都有大改, 有时候能优化掉一个复杂度, 而且越快的版本貌似码量越少.
开始的开始, 写了二维的 DP, 表示第 个格子是第 个连续的白格子的方案数, 结果发现转移非常麻烦, 于是就弃了.
color-40'.cpp
然后考虑用 表示第 个格子涂黑, 到 不涂黑的方案数, 写出方程:
然后发现如果要得到正确的答案, 可能会出现负数, 因为 的答案要从 转移过来. 为了防止 RE
, 只能把坐标向右平移 .
对于每个 都会进行一次 的 DP, 将询问离线, 按 排序, , 得到了 数组.
对于点 是不涂色的, 分两部分考虑, 枚举它左边的最近的黑点 , 右边最近的黑点 . 对于每对 的点, 答案就是 , 不要忘了左边不涂, 右边不涂和左右都不涂的情况.
写了非常麻烦的版本, 标称 , 实测 , 因为有一半的点 WA
掉了, 应该是因为我的思路太繁杂, 有一点细节没有处理好.
总复杂度
unsigned n, m, Ans(0), f[1005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return this->Quek < x.Quek;
}
}Q[100005];
int main() {
n = RD(), m = RD();
for (register unsigned i(1); i <= m; ++i) {
Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
}
sort(Q + 1, Q + m + 1);
for (register unsigned T(1); T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
memset(f, 0, sizeof(f));
f[1] = 1;
for (register unsigned i(Q[T].Quek); i <= n + Q[T].Quek; ++i) {
for (register unsigned j(1); j <= i - Q[T].Quek; ++j) {
f[i] += f[j];
if(f[i] >= MOD) f[i] -= MOD;
}
}
}
register unsigned long long TmpA(1);
for (register unsigned i(1 + Q[T].Quek); i <= n + Q[T].Quek - Q[T].Quex; ++i) {
TmpA += f[i];
if(TmpA >= MOD) TmpA -= MOD;
}
for (register unsigned i(1 + Q[T].Quek); i < Q[T].Quex + Q[T].Quek; ++i) {
TmpA += f[i];
if(TmpA >= MOD) TmpA -= MOD;
}
for (register unsigned i(1 + Q[T].Quek); i < Q[T].Quex + Q[T].Quek; ++i) {
for (register unsigned j(1 + Q[T].Quek); j <= min(n + Q[T].Quek - Q[T].Quex, n + Q[T].Quek + 1 - i); ++j) {
TmpA = (TmpA + (unsigned long long)f[i] * f[j]) % MOD;
}
}
Q[Q[T].Num].Ans = TmpA;
}
for (register unsigned i(1); i <= m; ++i) {
printf("%u\n", Q[i].Ans);
}
return 0;
}
color-50'Ex.cpp
这个版本已经能过 40'
了, 显然我对我的时间复杂度没有准确的估计.
这个版本的更新是设 , 用前缀和优化将每次求 的 DP 降到了 , 的左右不选情况降到了 , 单次询问降到了 , 所以时间复杂度来到了 . 这是里程碑式的飞跃.
unsigned n, m, Ans(0), f[100005], Sum[100005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return (this->Quek ^ x.Quek) ? (this->Quek < x.Quek) : (this->Quex < x.Quex);
}
}Q[100005];
int main() {
n = RD(), m = RD();
for (register unsigned i(1); i <= m; ++i) {
Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
}
sort(Q + 1, Q + m + 1);
for (register unsigned T(1); T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
memset(f, 0, sizeof(f)), f[1] = 1, Sum[0] = 0;
for (register unsigned i(Q[T].Quek), j(1), TmpFj(0); i <= n + Q[T].Quek; ++i) {
while (j <= i - Q[T].Quek) {TmpFj += f[j], ++j; if(TmpFj >= MOD) TmpFj -= MOD;}
f[i] += TmpFj; if(f[i] >= MOD) f[i] -= MOD;
}
for (register unsigned i(1); i <= n + Q[T].Quek; ++i) {Sum[i] = Sum[i - 1] + f[i]; if(Sum[i] >= MOD) Sum[i] -= MOD;}
}
register unsigned long long TmpA(1);//都不选
TmpA += Sum[n + Q[T].Quek - Q[T].Quex] - 1;
if(TmpA >= MOD) TmpA -= MOD;
TmpA += Sum[Q[T].Quex + Q[T].Quek - 1] - 1;
if(TmpA >= MOD) TmpA -= MOD;
for (register unsigned i(Q[T].Quex + Q[T].Quek - 1), TmpFj(0), j(1 + Q[T].Quek); i > Q[T].Quek; --i) {
while (j <= min(n + Q[T].Quek - Q[T].Quex, n + Q[T].Quek + 1 - i)) {TmpFj += f[j],++j; if(TmpFj >= MOD) TmpFj -= MOD;}
TmpA = (TmpA + (unsigned long long)f[i] * TmpFj) % MOD;
}
Q[Q[T].Num].Ans = TmpA;
}
for (register unsigned i(1); i <= m; ++i) {
printf("%u\n", Q[i].Ans);
}
return 0;
}
color-50'Ex_Pro.cpp
很遗憾, 这个版本还是 .
重新审视我们的转移方程:
我们发现, 因为 , . 那么对于 , 对它做出贡献的值只有 (下标是平移之前的, 可能有负数, 平移后不会越界). 这样就可以改一下方程, 让数组下标从 开始, 无需平移.
方程的正确性是这样保证的: 原来的 所转移的情况是除了 点涂黑, 其它的点都留白的情况. 对于 , 显然这种情况都存在, 所以就拿出来当一个边界条件讨论, 避免了更多的转移.
所以这次更新是对代码难度的更新, 之前总是算下标恶心死人.
unsigned n, m, Ans(0), f[100005], Sum[100005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return (this->Quek ^ x.Quek) ? (this->Quek > x.Quek) : (this->Quex < x.Quex);
}
}Q[100005];
int main() {
n = RD(), m = RD();
for (register unsigned i(1); i <= m; ++i) {
Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
}
sort(Q + 1, Q + m + 1);
for (register unsigned T(1); T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
memset(f, 0, sizeof(f)), Sum[0] = 0;
for (register unsigned i(1), j(1), TmpFj(1); i <= n; ++i) {
while (j + Q[T].Quek <= i) {TmpFj += f[j], ++j; if(TmpFj >= MOD) TmpFj -= MOD;}
f[i] = TmpFj;
}
for (register unsigned i(1); i <= n; ++i) {Sum[i] = Sum[i - 1] + f[i]; if(Sum[i] >= MOD) Sum[i] -= MOD;}
}
register unsigned long long TmpA(1);//都不选
TmpA += Sum[n - Q[T].Quex];//不选左
if(TmpA >= MOD) TmpA -= MOD;
TmpA += Sum[Q[T].Quex - 1];//不选右
if(TmpA >= MOD) TmpA -= MOD;
for (register unsigned i(Q[T].Quex - 1), j(1), AddDown; i; --i) {
AddDown = n - max(Q[T].Quex, i + Q[T].Quek - 1);
if(AddDown > 0x3f3f3f3f) {AddDown = 0;}
TmpA = (TmpA + (unsigned long long)f[i] * Sum[AddDown]) % MOD;
}
Q[Q[T].Num].Ans = TmpA;
}
for (register unsigned i(1); i <= m; ++i) printf("%u\n", Q[i].Ans);
return 0;
}
color-50'Ex_Pro_Max.cpp
这个版本无愧它 Pro Max
的称号, 得了 , 这是因为对求答案时的部分进行了剪枝, 因为有时候, 对于 , 可以随便选, 不用枚举, 可以前缀和优化掉, 对于 的情况, 也可以随便选, 可以优化, 所以询问复杂度变成 , 相当于优化了很大的常数.
unsigned n, m, Ans(0), f[100005], Sum[100005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return (this->Quek ^ x.Quek) ? (this->Quek < x.Quek) : (this->Quex < x.Quex);
}
}Q[100005];
int main() {
n = RD(), m = RD();
for (register unsigned i(1); i <= m; ++i) {
Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
}
sort(Q + 1, Q + m + 1);
for (register unsigned T(1); T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
memset(f, 0, sizeof(f)), Sum[0] = 0;
for (register unsigned i(1), j(1), TmpFj(1); i <= n; ++i) {
while (j + Q[T].Quek <= i) {TmpFj += f[j], ++j; if(TmpFj >= MOD) TmpFj -= MOD;}
f[i] = TmpFj;
}
for (register unsigned i(1); i <= n; ++i) {Sum[i] = Sum[i - 1] + f[i]; if(Sum[i] >= MOD) Sum[i] -= MOD;}
}
register unsigned long long TmpA(1);//都不选
TmpA += Sum[n - Q[T].Quex];//不选左
if(TmpA >= MOD) TmpA -= MOD;
TmpA += Sum[Q[T].Quex - 1];//不选右
if(TmpA >= MOD) TmpA -= MOD;
if(Q[T].Quex + 1 > Q[T].Quek) {
if(Q[T].Quek < 2) TmpA = (TmpA + (unsigned long long)Sum[Q[T].Quex - 1] * Sum[n - Q[T].Quex]) % MOD;
else {
TmpA = (TmpA + (unsigned long long)(Sum[Q[T].Quex - Q[T].Quek + 1]) * Sum[n - Q[T].Quex]) % MOD;
for (register unsigned i(Q[T].Quex - 1), AddDown; i > Q[T].Quex - Q[T].Quek + 1; --i) {
AddDown = n + 1 - i - Q[T].Quek;
if(AddDown > 0x3f3f3f3f) AddDown = 0;
TmpA = (TmpA + (unsigned long long)f[i] * Sum[AddDown]) % MOD;
}
}
} else {
for (register unsigned i(Q[T].Quex - 1), AddDown; i; --i) {
AddDown = n - max(Q[T].Quex, i + Q[T].Quek - 1);
if(AddDown > 0x3f3f3f3f) AddDown = 0;
TmpA = (TmpA + (unsigned long long)f[i] * Sum[AddDown]) % MOD;
}
}
Q[Q[T].Num].Ans = TmpA;
}
for (register unsigned i(1); i <= m; ++i) printf("%u\n", Q[i].Ans);
return 0;
}
color-70'Ex_Pro_Max_Ti.cpp
这个 70'
非常的丢人, 因为它只有 .
优化力度不大, 代码方面减少了讨论, 将两种情况合为一种, 因为两种唯一的区别是枚举 的上界. 时间方面只是剪枝了一下, 利用了求答案时利用下标 的单调性, 实现了 的情况自动跳过, 也体现在 的枚举上界上, 询问的复杂度变得十分玄学 or or or ... 但是它短呀!
unsigned n, m, Ans(0), f[100005], Sum[100005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return (this->Quek ^ x.Quek) ? (this->Quek < x.Quek) : (this->Quex < x.Quex);
}
}Q[100005];
int main() {
n = RD(), m = RD();
for (register unsigned i(1); i <= m; ++i) Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
sort(Q + 1, Q + m + 1);
for (register unsigned T(1); T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
memset(f, 0, sizeof(f)), Sum[0] = 0;
for (register unsigned i(1), j(1), TmpFj(1); i <= n; ++i) {
while (j + Q[T].Quek <= i) {TmpFj += f[j], ++j; if(TmpFj >= MOD) TmpFj -= MOD;}
f[i] = TmpFj;
}
for (register unsigned i(1); i <= n; ++i) {Sum[i] = Sum[i - 1] + f[i]; if(Sum[i] >= MOD) Sum[i] -= MOD;}
}
register unsigned long long TmpA(1);//都不选
register unsigned Flor(min(Q[T].Quex - 1, n + 1 - Q[T].Quek)), Ceil(0);
TmpA += Sum[n - Q[T].Quex];//不选左
if(TmpA >= MOD) TmpA -= MOD;
TmpA += Sum[Q[T].Quex - 1];//不选右
if(TmpA >= MOD) TmpA -= MOD;
if(Q[T].Quex + 1 > Q[T].Quek)
Ceil = Q[T].Quex - Q[T].Quek + 1, TmpA = (TmpA + (unsigned long long)Sum[Q[T].Quex - max(Q[T].Quek - 1, _1)] * Sum[n - Q[T].Quex]) % MOD;
for (register unsigned i(Flor), AddDown(n + 1 - i - Q[T].Quek); i > Ceil; --i, ++AddDown) TmpA = (TmpA + (unsigned long long)f[i] * Sum[AddDown]) % MOD;
Q[Q[T].Num].Ans = TmpA;
}
for (register unsigned i(1); i <= m; ++i) printf("%u\n", Q[i].Ans);
return 0;
}
color-70'Ex_Pro_Max_Ti_X.cpp
重新审视这个询问的过程, 发现所有的方案数 , 减去 涂黑的方案数 , 就是不涂 的方案数.
所以变成了 询问. 这是复杂度的巨大飞跃, 也是代码的进一步缩减, 但是更遗憾的是, 它还是只有
unsigned n, m, Ans(0), f[100005], Sum[100005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return (this->Quek ^ x.Quek) ? (this->Quek < x.Quek) : (this->Quex < x.Quex);
}
}Q[100005];
int main() {
n = RD(), m = RD();
for (register unsigned i(1); i <= m; ++i)
Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
sort(Q + 1, Q + m + 1);
for (register unsigned T(1); T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
memset(f, 0, sizeof(f)), Sum[0] = 0;
for (register unsigned i(1), j(1), TmpFj(1); i <= n; ++i) {
while (j + Q[T].Quek <= i) {TmpFj += f[j], ++j; if(TmpFj >= MOD) TmpFj -= MOD;}
f[i] = TmpFj;
}
for (register unsigned i(1); i <= n; ++i) {Sum[i] = Sum[i - 1] + f[i]; if(Sum[i] >= MOD) Sum[i] -= MOD;}
}
Q[Q[T].Num].Ans = (MOD + (Sum[n] + 1) - ((unsigned long long)f[Q[T].Quex] * f[n - Q[T].Quex + 1] % MOD));
if(Q[Q[T].Num].Ans >= MOD) Q[Q[T].Num].Ans -= MOD;
}
for (register unsigned i(1); i <= m; ++i) printf("%u\n", Q[i].Ans);
return 0;
}
color-70'Ex_Pro_Max_Ti_X_S.cpp
这个 70'
实测得了 . (No -O2
) 也是全场唯一的 (为什么我总是得这么奇怪的分啊?)
这是我考试时最后一个版本, 这份代码主要是对 DP 的过程进行剪枝, 因为打表可得, 对于某个 , 等于 . 又因为排序后 单增, 所以我们自然不用每次再求一遍 了, 复杂度变成 , 对于 大的情况下理论上能将整个程序复杂度优化 左右. 所以总复杂度 .
unsigned n, m, Ans(0), f[100005], Sum[100005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return (this->Quek ^ x.Quek) ? (this->Quek < x.Quek) : (this->Quex < x.Quex);
}
}Q[100005];
int main() {
n = RD(), m = RD();
for (register unsigned i(1); i <= m; ++i)
Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
sort(Q + 1, Q + m + 1);
for (register unsigned T(1); T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
for (register unsigned i(Q[T - 1].Quek + 1); i <= Q[T].Quek; ++i) f[i] = 1, Sum[i] = i;
for (register unsigned i(Q[T].Quek + 1); i <= n; ++i) {
f[i] = Sum[i - Q[T].Quek] + 1;
Sum[i] = Sum[i - 1] + f[i]; if(Sum[i] >= MOD) Sum[i] -= MOD;
}
}
Q[Q[T].Num].Ans = (MOD + (Sum[n] + 1) - ((unsigned long long)f[Q[T].Quex] * f[n - Q[T].Quex + 1] % MOD));
if(Q[Q[T].Num].Ans >= MOD) Q[Q[T].Num].Ans -= MOD;
}
for (register unsigned i(1); i <= m; ++i) printf("%u\n", Q[i].Ans);
return 0;
}
color-100'Ex_Pro_Max_Ti_X_Ultra_Extreme.cpp
在写正解之前, 先来看一下以张业琛为代表的选手的 做法. 我一听也很疑惑, 我的算法 在分子上, 为什么你的 在分子上?
我们知道一个黑点前面一定有 个连续的白点, 所以我们可以把这些白点和对应的黑点绑到一起, 成为一个长度为 的段, 值得一提的是, 由于第一个黑点前面不一定有 个白点, 所以可以在格子 前面加 的格子, 可以作为容纳第一个黑点对应白点的位置, 这样我们就有最多 个黑点了.
考虑有 个点, 那么它们占用的长度是 , 则还剩下 个自由的, 没有约束的白点, 像放哪放哪. 这时, 把黑点加上它们对应的尾巴看成一个点, 和自由白点加在一起做组合数即可求出方案数. 点数是 , 从这些点中选 个点定为带尾巴的黑点, 剩下的是自由白点的方案数是 .
对于 求 , 就能得到对于 长度和 间隔没有约束条件的合法涂色方案数.
接下来考虑 点为白的情况, 仍然是前面的想法, 即所有的情况减去 为黑的情况.
求 为黑的情况, 我们知道 必须是白点. 所以 右边最多有 , 左边最多有 . 对于 由于黑点的尾巴在左边, 所以带尾巴的黑点可以贴脸放, 而且不需要像求整段一样在左端加上 个点, 则 右边放 个黑点的合法方案数为 , 则 的左边放 个的合法方案数为 .
也就是说, 对于询问 , , 答案是:
只要可以 求二项式系数, 我们就可以 回答询问了.
众所周知, 二项式系数:
我们只要预处理出 和 即可, 求阶乘没问题, 主要问题是逆元.
要预处理 , 我们需要先用快速幂求出 .
假设 已经求出, 要求 使得 .
则 , 又因为 , 所以 .
这样, 总的复杂度就是 了.
接下来考虑正解.
我们现在有算法可以做到 , 也有算法能做到 . 所以可以数据分治, 对于 的询问, 用前一种算法, 对于 的做法, 用后一种做法, 这样, 最坏的复杂度是 .
最后是代码:
unsigned n, m, Ans(0), f[100005], Sum[100005], T(1), Sq;
unsigned long long Inv[200005], Fac[200005];
char flg(0);
struct Que {
unsigned Quex, Quek, Num, Ans;
inline const char operator<(const Que &x) const{
return (this->Quek ^ x.Quek) ? (this->Quek < x.Quek) : (this->Quex < x.Quex);
}
}Q[100005];
int main() {
n = RD(), m = RD(), Sq = sqrt(n);
for (register unsigned i(1); i <= m; ++i) Q[i].Quex = RD(), Q[i].Quek = RD(), Q[i].Num = i;
sort(Q + 1, Q + m + 1), Fac[0] = 1;
for (register unsigned i(1); i <= n + Q[m].Quek; ++i) Fac[i] = Fac[i - 1] * i % MOD;
register unsigned TmpM(MOD - 2);
register unsigned long long TmpFac(Fac[n + Q[m].Quek]);
Inv[n + Q[m].Quek] = 1;
while (TmpM) {
if(TmpM & 1) Inv[n + Q[m].Quek] = Inv[n + Q[m].Quek] * TmpFac % MOD;
TmpFac = TmpFac * TmpFac % MOD, TmpM >>= 1;}
for (register unsigned i(n + Q[m].Quek - 1); i < 0x3f3f3f3f; --i) Inv[i] = Inv[i + 1] * (i + 1) % MOD;
for (; Q[T].Quek <= Sq && T <= m; ++T) {
if(Q[T].Quek ^ Q[T - 1].Quek){
for (register unsigned i(Q[T - 1].Quek + 1); i <= Q[T].Quek; ++i) f[i] = 1, Sum[i] = i;
for (register unsigned i(Q[T].Quek + 1); i <= n; ++i){
f[i] = Sum[i - Q[T].Quek] + 1, Sum[i] = Sum[i - 1] + f[i]; if(Sum[i] >= MOD) Sum[i] -= MOD;}
}
Q[Q[T].Num].Ans = (MOD + (Sum[n] + 1) - ((unsigned long long)f[Q[T].Quex] * f[n - Q[T].Quex + 1] % MOD));
if(Q[Q[T].Num].Ans >= MOD) Q[Q[T].Num].Ans -= MOD;
}
for (register unsigned long long TmpTotal(1), TmpLe(1), TmpRi(1); T <= m; ++T) {
for (register unsigned i((n + Q[T].Quek - 1)/ Q[T].Quek), Len(n + Q[T].Quek - 1 - Q[T].Quek * i + i); i; --i, Len += (Q[T].Quek - 1))
TmpTotal = (TmpTotal + (Fac[Len] * Inv[i] % MOD) * Inv[Len - i]) % MOD;
for (register unsigned i((n - Q[T].Quex)/ Q[T].Quek), Len(n - Q[T].Quex - Q[T].Quek * i + i); i; --i, Len += (Q[T].Quek - 1))
TmpLe = (TmpLe + (Fac[Len] * Inv[i] % MOD) * Inv[Len - i]) % MOD;
for (register unsigned i((Q[T].Quex - 1) / Q[T].Quek), Len(Q[T].Quex - 1 - Q[T].Quek * i + i); i; --i, Len += (Q[T].Quek - 1))
TmpRi = (TmpRi + (Fac[Len] * Inv[i] % MOD) * Inv[Len - i]) % MOD;
Q[Q[T].Num].Ans = MOD + TmpTotal - (TmpLe * TmpRi % MOD), TmpTotal = TmpLe = TmpRi = 1;
if(Q[Q[T].Num].Ans >= MOD) Q[Q[T].Num].Ans -= MOD;
}
for (register unsigned i(1); i <= m; ++i) printf("%u\n", Q[i].Ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具