雅礼国庆集训 day1 T1 养花
题面
算法
考虑当 \(k\) 确定的时候如何求答案,
显然对于所有形如 \([ak, (a+1)k)\) 的值域区间, 最大值一定是最优的
似乎怎么都是 \(O(n^2)\) 的算法
观察到 \(a_i\) 的值域比较小, 所以考虑桶
显然对于一段区间 \([L, R]\)
我们可以推出其 \(mod k\) 的最大值
- 方法
首先用一个数组 \(f_i (\forall i \leq 10^5)\) 表示比 \(i\) 小的最大的数
这里有一个 \(\rm{unbelievable}\) 的递推方法( \(O(N + n)\) )
for (int i = l; i <= r; i++)
f[a[i]] = a[i];
for (int i = 1; i <= N; i++) // N 为值域
f[i] = std::max(f[i], f[i - 1]);
接下来可以通过这一递推公式求出这段区间对 \(i\) 取余的最大值
\[g_i = \max (f_{i \times j - 1} \mod i), i \times j \leq N
\]
(这里注意 \(j\) 得从 \(1\) 开始取)
在代码中的实现优化掉了 \(mod\) 运算
对于每一个块
计算是调和级数的复杂度, 也即 \(O(N \ln N)\)
但是此时计算单个区间的时间复杂度仍然是 \(10^5\) 级别的, 不可能在乘以一个 \(m\)
考虑分块, 这样能在 \(O(m \sqrt{n} + n \sqrt{n} \log n)\) 的复杂度下草过去(然而草不过去, 会TLE)
由此分析分块的最优复杂度
分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度
由于时间复杂度的总和是
\[O(\frac{n}{Size}N \ln N + mSize) = O(\frac{n ^ 2 \ln n}{Size} + mSize)
\]
均值不等式求得
\(Size\) 最优为 \(\sqrt{n \ln n}\)
复杂度为 \(O(n \sqrt{n \ln n})\), 当然常数拉满了
为什么正解这么暴力
代码
#include <bits/stdc++.h>
const int N = 1e5;
const int MAXN = 2e5 + 20;
const int MAXBLOCKSIZE = 500;
int n, m;
int a[MAXN];
void solve1()
{
while (m--)
{
int l, r, k;
scanf("%d %d %d", &l, &r, &k);
int Ans = 0;
for (int i = l; i <= r; i++)
{
Ans = std::max(Ans, a[i] % k);
}
printf("%d\n", Ans);
}
}
struct block
{
int Max_Num_Before[MAXN];
int Max_Mod[MAXN];
int Size;
int start, end;
} Block[MAXBLOCKSIZE];
int pos[MAXN];
void Calc(int i);
void Split_and_Init()
{
int block_Size = sqrt(n * log(n));
int block_Num = n / block_Size;
if (n % block_Size)
block_Num++;
for (int i = 1; i < block_Num; i++)
{
Block[i].start = (i - 1) * block_Size + 1;
Block[i].end = i * block_Size;
Calc(i);
}
Block[block_Num].start = (block_Num - 1) * block_Size + 1;
Block[block_Num].end = n;
Calc(block_Num);
for (int i = 1; i <= n; i++)
{
pos[i] = (i - 1) / block_Size + 1;
}
}
int Find(int l, int r, int k)
{
int Left_Block = pos[l];
int Right_Block = pos[r];
int Ans = 0;
if (Left_Block == Right_Block)
{
for (int i = l; i <= r; i++)
{
Ans = std::max(Ans, a[i] % k);
}
return Ans;
}
else
{
for (int i = Left_Block + 1; i <= Right_Block - 1; i++)
{
Ans = std::max(Ans, Block[i].Max_Mod[k]);
}
for (int i = l; i <= Block[Left_Block].end; i++)
{
Ans = std::max(Ans, a[i] % k);
}
for (int i = Block[Right_Block].start; i <= r; i++)
{
Ans = std::max(Ans, a[i] % k);
}
return Ans;
}
}
void solve2()
{
Split_and_Init();
while (m--)
{
int l, r, k;
scanf("%d %d %d", &l, &r, &k);
printf("%d\n", Find(l, r, k));
}
}
int main()
{
// freopen("flower.in", "r", stdin);
// freopen("flower.out", "w", stdout);
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
}
if (n <= 4000 && m <= 4000)
{
solve1();
}
else
{
solve2();
}
return 0;
}
void Calc(int i)
{
memset(Block[i].Max_Mod, 0, sizeof(Block[i].Max_Mod));
memset(Block[i].Max_Num_Before, 0, sizeof(Block[i].Max_Num_Before));
for (int j = Block[i].start; j <= Block[i].end; j++)
{
Block[i].Max_Num_Before[a[j]] = a[j];
}
for (int j = 1; j <= N; j++)
{
Block[i].Max_Num_Before[j] = std::max(Block[i].Max_Num_Before[j], Block[i].Max_Num_Before[j - 1]);
}
for (int j = 1; j <= N; j++)
{
for (int k = 0; k <= N; k += j) //
{
Block[i].Max_Mod[j] = std::max(Block[i].Max_Mod[j], Block[i].Max_Num_Before[std::min(k + j - 1, N)] - k);
}
}
}
/*
5 5
1 2 3 4 5
1 3 2
1 3 3
1 4 4
5 5 5
3 5 3
1
2
3
0
2
*/
总结
思路
抽象套路, 伟大的递推
分块 \(Size\) 的套路 : 初始化的时间复杂度 + 查询的时间复杂度
代码
实现代码之前把要用的公式先写好