2021牛客OI赛前集训营-提高组(第一场)补题
得分:\(80 + 20 + 0 + 37.5 = 137.5\)
排名:\(51\)
第一题 \(n^3\) 能骗 \(80\) 是我没想到的,第四题暴力最多能到 \(95\) 也是我没想到的。
A
本来以为是什么牛逼数论做法
发现 \(P \le 2000\),考虑把所有 \(i \to i * j \bmod p\) 连一条边权为 \(\mid i-j \mid\) 的边,然后跑最短路。然后 \(O(n^3)\) 预处理后就可以 \(n^2\) 遍历一遍求出答案。
正解是你通过打表发现 \(P \in [1,2000)\),所有的 \(ans(i,j)\) 都没有超过 \(17\),然后枚举 \(j\) 的时候只需要在 \([i-20,i+20]\) 之间枚举就好了。此时跑最短路建议 Dij or SPFA。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 4e6+500;
const int INF = 1e9+7;
const int mod = 998244353;
struct edge {
int to, w, nxt;
}e[MAXN];
int head[2020], num_edge = 1;
struct node {
int id, val;
bool operator < (const node &b) const { return val > b.val; }
};
int P, t, ans = 0;
int dis[2020], Pow[MAXN];
bool vis[2020];
int read(){
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
void add_edge(int from, int to, int w) { e[++num_edge] = (edge){to, w, head[from]}, head[from] = num_edge; }
void SPFA(int S) {
for(int i = 0; i <= P; ++i) dis[i] = 0x3f3f3f3f3f3f3f3f, vis[i] = false;
priority_queue<node> q;
q.push((node){S, 0}), dis[S] = 0;
while(!q.empty()) {
int u = q.top().id; q.pop();
if(vis[u]) continue;
vis[u] = true;
for(int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(dis[v] > dis[u] + e[i].w) {
dis[v] = dis[u] + e[i].w;
if(!vis[v]) q.push((node){v, dis[v]});
}
}
}
}
signed main()
{
P = read(), t = read();
Pow[0] = 1;
for(int i = 1; i <= P * P; ++i) Pow[i] = Pow[i - 1] * t % mod;
// for(int i = 0; i <= P * P; ++i) cout<<Pow[i]<<"\n";
for(int i = 1; i < P; ++i) {
int l = max(1ll, i - 20), r = min(P - 1, i + 20);
for(int j = l; j <= r; ++j) {
int v = i * j % P;
add_edge(i, v, abs(i - j));
}
}
for(int i = 1; i < P; ++i) {
// cout<<i<<"\n";
SPFA(i);
for(int j = 1; j < P; ++j) {
ans = ans + dis[j] * Pow[(i - 1) * (P - 1) + j - 1] % mod;
ans %= mod;
}
}
printf("%lld\n", ans);
return 0;
}
B
发现这个操作就相当于选一个点然后把序列分成两段。
所以考虑区间 DP。
设 \(f_{l,r}\) 表示操作 \([l,r]\) 区间的最小代价,\(g_{l,r}\) 来计数,\(m_{l,r}\) 表示 \([l,r]\) 区间的最大值。有转移方程:
时间复杂度 \(\mathcal O(n^3)\)。
实际上每次先操作区间最大值是最优的,因此没有必要对区间的所有数都进行枚举,而只枚举区间最大值。会得到常数上的优化,随机数据下跑的很快,时间复杂度 \(\mathcal O(\text{能过})\)。
实际上还有一个发掘性质来进行的优化可以将复杂度降到 \(\mathcal O((\frac{n}{2})^3)\)
对于一段区间 \([l,r]\),如果存在 \({a_i=a_{i+1}=\max\limits_{i=l}^r(a_i)(i\in[l,r))}\)
此时 \({i,i+1}\) 谁先选择没有关系,因此有 \({g_{l,r}=g_{l,i} \times g_{i+1,r} \times \binom{r-l}{i-l}}\)
因此当碰到两个最大值连续出现时,直接将整个区间划分为两段,最大值不连续则仍然枚举所有最大值。
然而我没补这个做法。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e3+50;
const int INF = 1e18+7;
const int mod = 998244353;
int n;
int a[MAXN];
int f[MAXN][MAXN], g[MAXN][MAXN], Max[MAXN][MAXN];
int pos[MAXN][MAXN];
int fac[MAXN], inv[MAXN];
int nxt[MAXN], pre[MAXN];
bool vis[MAXN][MAXN];
int read(){
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
void Init() {
fac[0] = fac[1] = inv[0] = inv[1] = 1;
for(int i = 2; i <= 1000; ++i) {
fac[i] = fac[i - 1] * i % mod;
inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
for(int i = 2; i <= 1000; ++i) {
inv[i] = inv[i] * inv[i - 1] % mod;
}
}
int C(int n, int m) {
if(n < m) return 0;
return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
void dfs(int l, int r) {
if(l >= r) {
f[l][r] = 0, g[l][r] = 1;
return ;
}
if(vis[l][r]) return ;
vis[l][r] = true;
f[l][r] = INF, g[l][r] = 0;
for(int i = pos[l][r]; i <= r; i = nxt[i]) {
dfs(l, i - 1), dfs(i + 1, r);
// f[l][r] = min(f[l][r], f[l][i - 1] + Max[l][i - 1] + f[i + 1][r] + Max[i + 1][r]);
g[l][r] = (g[l][r] + g[l][i - 1] * g[i + 1][r] % mod * C(r - l, i - l) % mod) % mod;
}
// for(int i = l; i <= r; ++i) {
// if(f[l][r] == f[l][i - 1] + Max[l][i - 1] + f[i + 1][r] + Max[i + 1][r]) {
// }
// }
}
signed main()
{
Init();
n = read();
for(int i = 1; i <= n; ++i) a[i] = read();
for(int i = 1; i <= n + 1; ++i) pre[i] = n + 1;
for(int i = n; i >= 1; --i) {
nxt[i] = pre[a[i]];
pre[a[i]] = i;
}
for(int i = 1; i <= n; ++i) {
// Max[i][i] = a[i];
pos[i][i] = i;
for(int j = i + 1; j <= n; ++j) {
if(a[pos[i][j - 1]] < a[j]) {
// Max[i][j] = a[j];
pos[i][j] = j;
} else {
pos[i][j] = pos[i][j - 1];
}
}
}
dfs(1, n);
printf("%lld\n", g[1][n]);
return 0;
}
C
直接粘题解吧,这个方向应该挺好找。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define int long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1e7+5;
const int INF = 1e9+7;
const int mod = 998244353;
const int Inv2 = 499122177;
const int Max = 1e7;
char s[MAXN];
int c;
int f[MAXN];
int read(){
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
int Calc(int x, int y, int n) {
return (x * n % mod + y * n % mod * (n + mod - 1) % mod * Inv2 % mod) % mod;
}
signed main()
{
f[0] = 1;
for(int i = 1; i <= Max; ++i) f[i] = (f[i - 1] << 1) % mod;
int T = read();
while(T--) {
cin >> s + 1; c = read();
int len = strlen(s + 1);
--c;
if(!c) {
int ans = 0;
for(int i = 1; i <= len; ++i) ans = ((ans << 1) + s[i] - '0') % mod;
ans = ans * (ans + 1) % mod * Inv2 % mod;
printf("%lld\n", ans);
continue;
}
if(c & 1) {
puts("0");
continue;
}
int p = 0;
while(c % 2 == 0) ++p, c /= 2;
int ans = 0;
for(int t = 0; t < len; ++t) {
int g = max(0ll, t + 1 - p);
if(t < len - 1) {
ans = (ans + f[g] * Calc(f[t], f[g], f[t + 1 - g] - f[t - g] + mod) % mod) % mod;
} else {
int tot = 0;
for(int i = 1; i <= len - g; ++i) tot = ((tot << 1) + s[i] - '0') % mod;
tot = (tot + 1 - f[t - g] + mod) % mod;
ans = (ans + f[g] * Calc(f[t], f[g], tot)) % mod;
int lst = (f[t] + (tot + mod - 1) * f[g] % mod) % mod;
int l = 0;
for(int i = 1; i <= len; ++i) l = ((l << 1) + s[i] - '0') % mod;
int r = (lst + f[g] + mod - 1) % mod;
ans = (ans - (r - l + mod) % mod * lst % mod + mod) % mod;
}
}
printf("%lld\n", ans);
}
return 0;
}
D
我们考虑 ST表 + bitset。
因为对于每个点可以二分找到不同种类数超过 \(k\) 的位置。
然后我们枚举左上角的点利用 ST表 二分就得到了 \(\le k\) 的情况有多少种,在跑一遍 \(\le k-1\) 的情况数相减即可。
/*
Work by: Suzt_ilymics
Problem: 不知名屑题
Knowledge: 垃圾算法
Time: O(能过)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<bitset>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl
using namespace std;
const int MAXN = 1505;
const int INF = 1e9+7;
const int mod = 1e9+7;
int n, m, K;
int Log[MAXN];
bitset<100> f[11][MAXN][MAXN];
int read(){
int s = 0, f = 0;
char ch = getchar();
while(!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
return f ? -s : s;
}
void ST() {
for(int k = 1, M = max(n, m); (1 << k) <= M; ++k) {
for(int i = 1; i + (1 << k) - 1 <= n; ++i) {
for(int j = 1; j + (1 << k) - 1 <= m; ++j) {
f[k][i][j] = f[k - 1][i][j] | f[k - 1][i + (1 << k - 1)][j] |
f[k - 1][i][j + (1 << k - 1)] | f[k - 1][i + (1 << k - 1)][j + (1 << k - 1)];
}
}
}
}
int Query(int sx, int sy, int ex, int ey) {
int k = Log[ex - sx + 1];
return (f[k][sx][sy] | f[k][ex - (1 << k) + 1][sy] |
f[k][sx][ey - (1 << k) + 1] | f[k][ex - (1 << k) + 1][ey - (1 << k) + 1]).count();
}
LL Calc(int Mid) {
if(!Mid) return 0;
LL ans = 0;
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= m; ++j) {
int l = 1, r = min(n - i + 1, m - j + 1), res = 0;
while(l <= r) {
int mid = (l + r) >> 1;
if(Query(i, j, i + mid - 1, j + mid - 1) <= Mid) {
res = mid, l = mid + 1;
} else {
r = mid - 1;
}
}
// cout<<"ck: "<<i<<" "<<j<<" "<<l<<"\n";
ans += res;
}
}
return ans;
}
signed main()
{
for(int i = 2; i <= 1500; ++i) Log[i] = Log[i >> 1] + 1;
n = read(), m = read(), K = read();
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= m; ++j) {
f[0][i][j][read() - 1] = true;
}
}
ST();
printf("%lld\n", Calc(K) - Calc(K - 1));
return 0;
}