2023.02.13 模拟赛小结
目录更好的阅读体验戳此进入
赛时思路
T1
给定序列,求序列有多少个排列满足任意相邻两数之积非完全平方数。
赛时最终想到的是质因数分解后,两数合成之后若所有质因数幂次均为偶数则为完全平方数,依此可以 $ O(n^2 \log^2 a) $ 处理每个数可以(或不可以)和哪些数相邻,这样的话如果求的不是排列而是任意序列那就是一个非常朴素的 DP 了,不过排列就完全没有想到什么复杂度正确的好做法,唯一想到了一个 $ O(n^5) $ 的不知道正确性的区间 DP 做法,即考虑 $ dp(l, r, x, y) $ 表示 $ [l, r] $ 生成的左端点为 $ x $ 右端点为 $ y $ 的排列,然后枚举分割点转移,当然这个东西既不知道正确性又没有部分分,所以直接考虑 next_permutation()
。期望 $ 10\mathrm{pts} $。
Code
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
#define MOD (ll)(1e9 + 7)
#define LIM (110000)
template < typename T = int >
inline T read(void);
int N;
int A[310];
struct Fact{int val, cnt;};
basic_string < Fact > facts[310];
basic_string < int > legal[310];
bitset < LIM + 100 > notPrime;
basic_string < int > Prime;
int main(){
freopen("square.in", "r", stdin);
freopen("square.out", "w", stdout);
N = read();
// for(int i = 2; i <= LIM; ++i){
// if(!notPrime[i])Prime += i;
// for(auto p : Prime){
// if((ll)i * p > LIM)break;
// notPrime[i * p] = true;
// if(i % p == 0)break;
// }
// }
for(int i = 1; i <= N; ++i){
int val = A[i] = read();
// for(auto p : Prime){
// if((ll)p * p > val)break;
// if(val % p == 0)facts[i] += Fact{p, 0};
// while(val % p == 0)facts[i].back().cnt++, val /= p;
// }if(val != 1)facts[i] += Fact{val, 1};
}
int ans(0);
int base(1);
for(int i = 1; i <= N; ++i)base *= i;
for(int i = 1; i <= base; ++i){
bool flag(true);
for(int j = 1; j < N; ++j){
ll val = (ll)A[j] * A[j + 1];
if((ll)sqrt(val) * (ll)sqrt(val) == val){flag = false; break;}
}if(flag)++ans;
next_permutation(A + 1, A + N + 1);
}printf("%d\n", ans);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
T2
CF819D Mister B and Astronomers。
存在一个星星每 $ T $ 秒闪烁一次,存在 $ n $ 个科学家围成一圈循环观测星星,第一个在 $ 0 $ 秒观测,第二个在 $ a_2 $ 秒观测,第三个在 $ a_2 + a_3 $ 秒观测,$ \cdots $ 第一个科学家在 $ a_1 + a_2 + a_3 + \cdots + a_n $ 观测,$ \cdots $。对于 $ t \in [0, T) $,若星星第一次闪烁在 $ t $ 秒,那么第一次观测到这个星星的科学家将会获得 $ 1 $ 的贡献,求每个科学家的贡献。
一道十分高妙的 $ \operatorname{exgcd} $ 题,赛时大概是发现了会根据同余成环,环数是 $ \gcd(\sum a, T) $,这样的话部分分就是暴力枚举每个 $ t $ 然后一直枚举是否出现与对应科学家同余并计算贡献即可,不难想到若两者互质那么一定有解,而在暴力分中两者不互质的部分可能存在不再同一环内导致无解,这里一个简单的办法就是均分时间然后卡时即可,期望得分 $ 20\mathrm{pts} $。
Code
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
template < typename T = int >
inline T read(void);
int N, T;
int A[210000];
ll sumA(0);
int ans[210000];
unordered_map < ll, int > equiv;
int main(){
freopen("moon.in", "r", stdin);
freopen("moon.out", "w", stdout);
T = read(), N = read();
for(int i = 1; i <= N; ++i)sumA += (A[i] = read());
equiv[0] = 1;
int curs(0);
for(int i = 2; i <= N; ++i)curs += A[i], equiv[curs] = i;
double clock_lim = 2.0 / (double)T;
for(int t = 0; t <= T - 1; ++t){
double sc = (double)clock() / CLOCKS_PER_SEC;
int cur(t % sumA);
while(equiv.find(cur) == equiv.end() && (double)clock() / CLOCKS_PER_SEC - sc <= clock_lim)(cur += T) %= sumA;
if(equiv.find(cur) != equiv.end())ans[equiv[cur]]++;
}
for(int i = 1; i <= N; ++i)printf("%d%c", ans[i], i == N ? '\n' : ' ');
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
T3
没找到原题。
大概就是存在一个一维空间,最小位置在 $ 1 $,最大位置为 $ n $,你可以随时减少位置,但只能使用 $ m $ 个单向传送门从 $ l_i $ 到 $ r_i \(,\) q $ 次询问给定限定 $ [x_i, y_i] $ 表示你初始在 $ x_i $,并位置最小不能小于 $ x_i $,最大不能大于 $ y_i $,求最大能到达哪个位置。
部分分给的还行,前 $ 20\mathrm{pts} $ 白送的,后面两个部分分是传送门无重叠和询问无重叠,怎么说呢,想到的几个思路最后都被我卡掉了,理论上满的时候都是 $ m^2 $ 的,不过数据比较弱又多过了 $ 20\mathrm{pts} $。
Code
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
template < typename T = int >
inline T read(void);
int N, M, Q;
// class SegTree{
// private:
// int cnt[110000 << 2];
// int sum[110000 << 2];
// int lz[110000 << 2];
// #define LS (p << 1)
// #define RS (LS | 1)
// #define MID ((gl + gr) >> 1)
// public:
// void Pushup(int p){
// sum[p] = sum[LS] + sum[RS];
// cnt[p] = cnt[LS] + cnt[RS];
// }
// void Pushdown(int p){
// if(!lz[p])return;
// }
// }
struct Sline{
int l, r;
friend const bool operator < (const Sline &a, const Sline &b){
return a.l == b.l ? a.r < b.r : a.l < b.l;
}
}s[110000];
int main(){
freopen("snail.in", "r", stdin);
freopen("snail.out", "w", stdout);
N = read(), M = read();
for(int i = 1; i <= M; ++i)s[i].l = read(), s[i].r = read();
sort(s + 1, s + M + 1);
Q = read();
if((ll)M * Q <= (ll)(1e8)){
while(Q--){
int l = read(), r = read();
int curl = l, curr = l;
for(int i = 1; i <= M; ++i)
if(curl <= s[i].l && s[i].l <= curr && s[i].r <= r)curr = max(curr, s[i].r);
printf("%d\n", curr);
}
}else{
bool spLine(true);
for(int i = 1; i <= M - 1; ++i)
if(s[i].l < s[i + 1].l && s[i + 1].l < s[i].r){spLine = false; break;}
if(spLine){
basic_string < int > ans[110000];
for(int i = M; i >= 1; --i){
ans[s[i].l] += s[i].r;
}
for(int i = N; i >= 1; --i){
basic_string < int > tmp = ans[i];
for(auto v : tmp)ans[i] += ans[v];
}
while(Q--){
int l = read(), r = read();
int anss(l);
for(auto v : ans[l])if(v <= r)anss = max(anss, v);
printf("%d\n", anss);
// for(int i = 1; i <= M; ++i)
// if(curl <= s[i].l && s[i].l <= curr && s[i].r <= r)curr = max(curr, s[i].r);
// printf("%d\n", curr);
}
}
}
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
正解
T1
首先有一个更高妙的转换,可以认为 $ 50% $ 的部分分是一个提示,即所有数都是质数的时候问题转化为不能有相同的数相邻,而正解可以先考虑将每个数的平方质因子全部去除,这样问题即可转化为要求相邻两数不同,考虑如何处理。
存在朴素 DP,考虑按序添加数(这个东西怎么处理赛时属实是想了好久假了好久),不难发现添加 $ a_{i + 1} $ 到 $ [1, i] $ 的排列时,有四种情况,即两侧均为 $ a_{i + 1} $,一侧为 $ a_{i + 1} $,两侧相同且均不为 $ a_{i + 1} $,两侧不同且均不为 $ a_{i + 1} $。
显然处理排列时原序列不同顺序本质相同,于是考虑将原序列排序,依次处理 $ s_i $ 表示前 $ i $ 个数里有多少个 $ a_{i + 1} $。
同时令 $ dp(i, j, k) $ 表示考虑前 $ i $ 个数,有 $ j $ 个两侧数相同且不为 $ a_{i + 1} $,有 $ k $ 个两侧数均为 $ a_{i + 1} $,显然对于前两种情况,考虑应有 $ 2 \times s_i $ 个位置,每存在一个相邻两侧均为 $ a_{i + 1} $ 的就会减少 $ 1 $,即转移为:
\[dp(i, j, k) \times (2 \times s_i - k) \rightarrow dp(i + 1, j, k + 1) \]对于第三种情况,同理:
\[dp(i, j, k) \times j \rightarrow dp(i + 1, j - 1, k) \]对于第四种情况,同理:
\[dp(i, j, k) \times (i + 1 - (2 \times s_i - k) - j) \rightarrow dp(i + 1, j, k) \]同时这里我们注意一个问题,当 $ a_i \neq a_{i + 1} $ 时,我们原来记录的 $ k $ 的意义会改变,换句话说原来的 $ k $ 会在新的这一次变为 $ j $,于是此时将所有 $ dp(i, j, k) $ 的贡献转移到 $ dp(i, j + k, 0) $ 即可。
最终答案显然为 $ dp(n, 0, 0) $,复杂度 $ O(n^3) $。
双倍经验 LG-P4448 [AHOI2018初中组]球球的排列。
Code
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
#define LIM (110000)
#define MOD (ll)(1e9 + 7)
template < typename T = int >
inline T read(void);
int N;
ll A[310];
ll s[310];
ll dp[2][310][310];
bitset < LIM + 100 > notPrime;
basic_string < int > Prime;
int main(){
for(int i = 2; i <= LIM; ++i){
if(!notPrime[i])Prime += i;
for(auto p : Prime){
if((ll)i * p > LIM)break;
notPrime[i * p] = true;
if(i % p == 0)break;
}
}
N = read();
for(int i = 1; i <= N; ++i){
A[i] = read();
for(auto p : Prime){
if((ll)p * p > A[i])break;
while(A[i] % (ll)(p * p) == 0)A[i] /= (ll)p * p;
}
}
sort(A + 1, A + N + 1);
int cnt(0);
for(int i = 1; i < N; ++i)
if(A[i] != A[i + 1])s[i] = 0, cnt = 0;
else s[i] = ++cnt;
dp[0][0][0] = 1; bool cur(0);
for(int i = 0; i < N; ++i){
memset(dp[cur ^ 1], 0, sizeof dp[cur ^ 1]);
if(A[i] != A[i + 1])
for(int j = 0; j <= N; ++j)
for(int k = 1; k <= N; ++k)
if(j + k <= N)(dp[cur][j + k][0] += dp[cur][j][k]) %= MOD, dp[cur][j][k] = 0;
for(int j = 0; j <= N; ++j)
for(int k = 0; k <= N; ++k){
if(2 * s[i] - k > 0)(dp[cur ^ 1][j][k + 1] += dp[cur][j][k] * (2 * s[i] - k) % MOD) %= MOD;
if(j - 1 >= 0)(dp[cur ^ 1][j - 1][k] += dp[cur][j][k] * j % MOD) %= MOD;
if(i + 1 - (2 * s[i] - k) - j >= 0)(dp[cur ^ 1][j][k] += dp[cur][j][k] * (i + 1 - (2 * s[i] - k) - j) % MOD) %= MOD;
}
cur ^= 1;
}printf("%lld\n", dp[cur][0][0]);
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
T2
首先不难想到,令 $ S = \sum a_i $,第 $ i $ 个科学家观测时间为 $ \sum_{j = 2}^i a_j + kS, k \in \mathbb{N} $,令 $ t_i = \sum_{j = 2}^i a_j $,同时对于 $ T $,假设本次初始时间为 $ \xi $,那么可以认为需要找到第一个满足 $ t_i + kS \equiv \xi \pmod{T} $。
对于后者式子不难发现其为经典的群论套路,即我们发现对于某个科学家的初始点 $ t_i $,可以认为每次模意义下步进 $ S $,此时会构成 $ \gcd(S, T) $ 个环,环长均为 $ \dfrac{T}{\gcd(S, T)} $,或者说每连续 $ \gcd(S, T) $ 个数均属于不同的环。关于这个性质似乎特别显然,不过这里我们也简单证明一下:
显然我们要证明的即为如下式子:
\[\forall i \in [0, T), i \equiv i + \gcd(S, T) \pmod{\gcd(S, T)} \]\[\forall i \in [0, T), t \in (0, \gcd(S, T)), i + t \not\equiv i \pmod{\gcd(S, T)} \]证明均显然。
此时我们考虑,对于一个环内的所有科学家,$ \xi $ 为环上某一点的贡献,我们要找到的就是第一个,或者说其减少最少个 $ S $ 遇到的 $ t_i $,这个东西不难想到,对于两个同环科学家 $ t_i, t_j $,且 $ t_j $ 在 $ t_i $ 之后,两者之间模意义下距离了多少个 $ S $ 就代表 $ i $ 的答案由多少。而对于不同环中显然互相无交无关联。
所以我们可以考虑对于一个环内的所有科学家 $ t_1, t_2, \cdots $,我们如果能够按照其在环内的遍历顺序排序,或者说满足 $ t_i $ 之后的 $ t_j $ 一定是 $ t_i $ 经过最小次模意义下加 $ S $ 的操作得到的(这里如何做我们后文再叙),然后遍历一遍,此时对于相邻的 $ t_i \lt t_j $(注意此时的 $ \lt $ 意义是我们重载过的小于号,且两者相邻或 $ i $ 为末尾 $ j $ 为初始)存在:
\[t_i + xS \equiv t_j \pmod{T} \]转化后得到:
\[Sx + Ty = t_j - t_i \]并且显然满足 $ \gcd(S, T) \mid t_j - t_i $,满足 exgcd 的限制,注意此时 $ y $ 是任意的,所以仅需求出 $ x $ 的最小非负整数解,其即代表了 $ t_i $ 对应科学家的总贡献,也就是答案。同时注意若环内仅有一个科学家则其贡献直接为 $ \dfrac{T}{\gcd(S, T)} $。
现在我们在将目光移回上文所述的排序过程,考虑发现该限制对应到上述方程的意义就是 $ x $ 最小的解,如果满足 $ S \perp T $ 那么我们是可以通过乘逆元转换为 $ t_i \times S^{-1} \bmod{T} $ 的偏序关系,但本题显然不满足,于是不难想到若其初始时位置为 $ p_i = t_i \bmod{\gcd(S, T)} $,那么一定存在 $ p_i + kS \equiv t_i \pmod{T} $,发现此方程亦为 exgcd 形式,转换为 $ Sx + Ty = t_i - p_i $,求出 $ x $ 的最小非负整数解即可得到其在环中对应的位置,以其为关键字进行排序则一定满足我们上述的偏序关系。而解相同的,即同环模意义下同位置的,不难想到我们优先保留 $ t_i $ 较小的即可,较后的一定为 $ 0 $。
最终复杂度卡在排序和两次 exgcd 上,为 $ O(n \log n) $。
Tips:还有些小细节,观察所有式子发现,对于所有 $ S $ 和 $ t_i $ 都可以并需要进行 $ S \leftarrow S \bmod{T}, t_i \leftarrow t_i \bmod{T} $,这是对答案无影响的。对于在 set
中排除同环模意义下同位置的,可以考虑利用 C++ 特性,重载小于号,使得只保留相同第一关键字中最先加入的,可以证明最先加入的一定最小。
附:好久没写 exgcd 了,简单重推一下吧:
要求 $ ax + by = \gcd(a, b) $。
显然存在 $ bx' + (a \bmod{b})y' = \gcd(b, a \bmod{b}) $。
又 $ \gcd(a, b) = \gcd(b, a \bmod{b}) $。
整理并展开 $ a \bmod b $ 得到 $ ax + by = bx' + (a - \lfloor \dfrac{a}{b} \rfloor \times b)y' $。
则显然有 $ x = y', y = x' - \lfloor \dfrac{a}{b} \rfloor \times y $。
问题规模缩减,不断递归,直到最终 $ \gcd = 0 $ 则回溯 $ x = 1, y = 0 $ 即可。
此时得到任意解,令 $ d = \dfrac{t_j - t_i}{\gcd(S, T)} $,显然整除,则 $ x \leftarrow x \times d, y \leftarrow y \times d $ 即可,同时注意我们想要求得 $ x $ 得最小非负解,发现方程通解为 $ x + k\dfrac{T}{\gcd(S, T)}, y - k\dfrac{S}{\gcd(S, T)} $,以 $ \dfrac{T}{\gcd(S, T)} $ 为模数对 $ x $ 取模转正后求出对应的 $ y $ 即可找到 $ x $ 的最小非负解。
Code
#define _USE_MATH_DEFINES
#include <bits/stdc++.h>
#define PI M_PI
#define E M_E
#define npt nullptr
#define SON i->to
#define OPNEW void* operator new(size_t)
#define ROPNEW void* Edge::operator new(size_t){static Edge* P = ed; return P++;}
#define ROPNEW_NODE void* Node::operator new(size_t){static Node* P = nd; return P++;}
using namespace std;
mt19937 rnd(random_device{}());
int rndd(int l, int r){return rnd() % (r - l + 1) + l;}
bool rnddd(int x){return rndd(1, 100) <= x;}
typedef unsigned int uint;
typedef unsigned long long unll;
typedef long long ll;
typedef long double ld;
template < typename T = int >
inline T read(void);
int N;
ll S, T;
int A[210000];
int ans[210000];
int gcdv(-1);
struct Node{
ll v; ll origin; int idx;
friend const bool operator < (const Node &a, const Node &b){
return a.v < b.v;
}
};
unordered_map < int, set < Node > > loops;
void exgcd(ll a, ll b, ll &x, ll &y){
if(!b)return x = 1, y = 0, void();
exgcd(b, a % b, x, y);
int tmp(x);
x = y, y = tmp - a / b * y;
}
ll CalMnX(ll ti, ll tj){
ll x, y; ll d = (tj - ti) / gcdv;
exgcd(S, T, x, y);
x *= d, y *= d;
ll P = T / gcdv;
x = (x % P + P) % P;
y = (tj - ti - S * x) / T;
return x;
}
int main(){
T = read(), N = read();
for(int i = 1; i <= N; ++i)(S += (A[i] = read())) %= T;
gcdv = __gcd(S, T);
loops[0].insert({Node{0, 0, 1}});
ll cur(0);
for(int i = 2; i <= N; ++i)
(cur += A[i]) %= T,
loops[cur % gcdv].insert(Node{CalMnX(cur % gcdv, cur), cur, i});
for(auto loop : loops){
if((int)loop.second.size() == 1){ans[loop.second.begin()->idx] = T / gcdv; continue;}
for(auto it = loop.second.begin(); it != loop.second.end(); advance(it, 1)){
auto nxit = it == prev(loop.second.end()) ? loop.second.begin() : next(it);
ans[it->idx] = CalMnX(it->origin, nxit->origin);
}
}
for(int i = 1; i <= N; ++i)printf("%d%c", ans[i], i == N ? '\n' : ' ');
fprintf(stderr, "Time: %.6lf\n", (double)clock() / CLOCKS_PER_SEC);
return 0;
}
template < typename T >
inline T read(void){
T ret(0);
int flag(1);
char c = getchar();
while(c != '-' && !isdigit(c))c = getchar();
if(c == '-')flag = -1, c = getchar();
while(isdigit(c)){
ret *= 10;
ret += int(c - '0');
c = getchar();
}
ret *= flag;
return ret;
}
T3
首先记录一下 @sssmzy 的部分分做法:
对于传送门无重叠,加个倍增即可保证复杂度。
对于询问无重叠,考虑暴力建图即可均摊正确复杂度。
正解分块或吉司机线段树。
//TODO (先去学吉司机线段树了。。
UPD
update-2023_02_13 初稿
标签:13,return,int,2023.02,ret,小结,void,dp,define From: https://www.cnblogs.com/tsawke/p/17124382.html