题意:给定一个长度为 n n n 的 01 01 01 串,每次可以选取一个奇数长度的连续子串进行位置上的翻转,问经过若干次操作能得到的字典序最小的串。 n ≤ 5 × 1 0 5 n \leq 5\times 10^5 n≤5×105。
解法:不难发现,处于奇数位置的数字永远在奇数位,处于偶数位的数字永远在偶数位,并且可以通过长度为 3 3 3 的连续子串不断翻转使得只有奇数或偶数位的数字在翻转。因而等效于奇数位置和偶数位置单独排序。
题意:给定一个互不相等的长度为 n n n 的序列 { a i } \{a_i\} {ai},将其划分为若干段并将每段都视为 1 1 1-base 的数组 ,使得每一段的最大值处于奇数位,每一段的最小值处于偶数位,问划分方案数。 n ≤ 3 × 1 0 5 n \leq 3\times 10^5 n≤3×105。
解法:考虑 f i f_i fi 表示前 i i i 个数字的划分方法,则 f i f_i fi 可以从所有满足上述条件的子段 [ j + 1 , i ] [j+1,i] [j+1,i] 中转移。考虑如何快速找到这些 j j j。若插入 a i a_i ai 后,当前的最大值所处位置为 k k k,则仅考虑最大值条件,与 k k k 同奇偶的 j j j 即可成功转移;对于最小值则是不同奇偶。因而需要支持只对奇数位置激活和只对偶数位置激活,查询同时满足两个条件的位置的和的数据结构。
利用单调栈来维护包含第 i i i 个数字的连续子段中最大值和最小值的位置,同时维护两个线段树分别表示奇数位置和偶数位置的 dp 值。标记分为最大值标记和最小值标记,查询的时候仅查询同时有两个标记的点。注意利用单调栈的性质,一个位置不会同时被多个最大值标记同时覆盖,因而可以直接简单相加所有的标记的和来表示标记种类。总时间复杂度 O ( n log n ) \mathcal O(n \log n) O(nlogn)。
#include
using namespace std;
const long long mod = 998244353;
const int N = 300000;
class segment_tree
{
struct node
{
long long sum, addtag;
int tag;
node()
{
sum = addtag = 0;
tag = 0;
}
};
int n;
node t[4 * N + 5];
void build(int place, int left, int right)
{
t[place].sum = t[place].addtag = t[place].tag = 0;
if (left == right)
return;
int mid = (left + right) >> 1;
build(place << 1, left, mid);
build(place << 1 | 1, mid + 1, right);
}
void pushup(int place)
{
t[place].tag = max(t[place << 1].tag, t[place << 1 | 1].tag);
t[place].sum = 0;
if (t[place].tag == t[place << 1].tag)
t[place].sum = (t[place].sum + t[place << 1].sum) % mod;
if (t[place].tag == t[place << 1 | 1].tag)
t[place].sum = (t[place].sum + t[place << 1 | 1].sum) % mod;
}
void pushdown(int place, int left, int right)
{
if (t[place].addtag)
{
int mid = (left + right) >> 1;
update(place << 1, left, mid, left, mid, t[place].addtag);
update(place << 1 | 1, mid + 1, right, mid + 1, right, t[place].addtag);
t[place].addtag = 0;
}
}
void update(int place, int left, int right, int start, int end, int x)
{
if (start <= left && right <= end)
{
t[place].tag += x;
t[place].addtag += x;
return;
}
pushdown(place, left, right);
int mid = (left + right) >> 1;
if (start <= mid)
update(place << 1, left, mid, start, end, x);
if (end > mid)
update(place << 1 | 1, mid + 1, right, start, end, x);
pushup(place);
}
void update(int place, int left, int right, int start, long long x)
{
if (left == right)
{
t[place].sum = x;
return;
}
pushdown(place, left, right);
int mid = (left + right) >> 1;
if (start <= mid)
update(place << 1, left, mid, start, x);
else
update(place << 1 | 1, mid + 1, right, start, x);
pushup(place);
}
public:
void build(int n)
{
this->n = n;
build(1, 1, n);
}
void update(int l, int r, int x)
{
update(1, 1, n, l, r, x);
}
void update(int pos, long long x)
{
update(1, 1, n, pos, x);
}
long long query()
{
if (t[1].tag == 2)
return t[1].sum;
else
return 0;
}
} T[2];
int st[2][N + 5], tp[2], a[N + 5];
long long f[N + 5];
int main()
{
int t, n;
scanf("%d", &t);
while(t--)
{
scanf("%d", &n);
T[0].build(n);
T[1].build(n);
for (int i = 1; i <= n;i++)
scanf("%d", &a[i]);
f[0] = 1;
for (int i = 1; i <= n; i++)
{
T[i & 1].update(i, f[i - 1]);
while (tp[0] && a[i] > a[st[0][tp[0]]])
{
T[st[0][tp[0]] & 1].update(st[0][tp[0] - 1] + 1, st[0][tp[0]], -1);
tp[0]--;
}
T[i & 1].update(st[0][tp[0]] + 1, i, 1);
st[0][++tp[0]] = i;
while (tp[1] && a[i] < a[st[1][tp[1]]])
{
T[(st[1][tp[1]] & 1) ^ 1].update(st[1][tp[1] - 1] + 1, st[1][tp[1]], -1);
tp[1]--;
}
T[(i & 1) ^ 1].update(st[1][tp[1]] + 1, i, 1);
st[1][++tp[1]] = i;
f[i] = (T[0].query() + T[1].query()) % mod;
}
printf("%lld\n", f[n]);
tp[0] = tp[1] = 0;
}
return 0;
}
题意:问最少需要画多少条不过原点的直线,使得可以经过 { ( i , j ) ∣ i , j ∈ [ 0 , n ] } / ( 0 , 0 ) \{(i,j)|i,j \in [0,n] \}/(0,0) {(i,j)∣i,j∈[0,n]}/(0,0)。
解法:作 2 n 2n 2n 条 x + y = b x+y=b x+y=b 的直线即可。故答案为 2 n 2n 2n。
题意:给定长度为 n n n 的序列 { a i } \{a_i\} {ai} 和 n − 1 n-1 n−1 条边,第 i i i 条边连接 ( i , i + 1 ) (i,i+1) (i,i+1)。每条边上有个质数 p i p_i pi,能通过它当且仅当已经走过的位置中的数字中有 p i p_i pi 的倍数。 q q q 次询问从 x x x 号节点出发能不能到达 y y y 号节点。 n , q ≤ 2 × 1 0 5 n,q \leq 2\times 10^5 n,q≤2×105。
解法:显然需要求出每个点能走到的范围,然后 O ( 1 ) \mathcal O(1) O(1) 查询。
如果暴力计算,则可以每次从一个点开始向两侧扩展。对于一条边是否可以经过,可以提前 O ( n ) \mathcal O(n) O(n) 的预处理其左右两侧最近是它倍数的数字位置,以实现 O ( 1 ) \mathcal O(1) O(1) 的查询。这样暴力的复杂度为 O ( n 2 ) \mathcal O(n^2) O(n2)。
考虑如何加速上述过程。首先当扩展到一个新点,可以将它所能扩展到的区域直接拿过来。此外,当当前节点 i i i 要进行左侧扩展时,对于扩展到的新点 i − 1 i-1 i−1 进行继续扩展,其搜索范围也仅限于点 i i i 的左侧,不会回头。向右扩展同理。具体细节参看下方代码。
(复杂度非常的玄学,不便于分析)。
#include
using namespace std;
const int N = 200000;
bool vis[N + 5];
int prime[N + 5], tot;
vector<int> factor[N + 5];
void sieve(int n)
{
for (int i = 2; i <= n;i++)
{
if(!vis[i])
{
prime[++tot] = i;
factor[i] = {i};
}
for (int j = 1; j <= tot && (long long)prime[j] * i <= n;j++)
{
int num = prime[j] * i;
vis[num] = 1;
factor[num] = factor[i];
if (i % prime[j])
factor[num].push_back(prime[j]);
else
break;
}
}
}
int main()
{
sieve(N);
int t, n, q, x, y;
scanf("%d", &t);
while(t--)
{
scanf("%d%d", &n, &q);
vector<int> llim(n), rlim(n), lbound(n), rbound(n);
vector<int> a(n), b(n - 1);
for (int i = 0; i < n;i++)
scanf("%d", &a[i]);
for (int i = 0; i < n - 1;i++)
scanf("%d", &b[i]);
vector<int> pos(N, -1);
for (int i = 0; i < n - 1;i++)
{
for (auto j : factor[a[i]])
pos[j] = i;
llim[i] = pos[b[i]];
}
for (auto &i : pos)
i = n + 1;
for (int i = n - 1; i >= 1;i--)
{
for (auto j : factor[a[i]])
pos[j] = i;
rlim[i - 1] = pos[b[i - 1]];
}
for (int i = 0; i < n;i++)
lbound[i] = rbound[i] = i;
function<void(int, int, int)> dfs = [&](int pos, int left, int right)
{
if (left > right)
return;
bool flag = 1;
while (flag)
{
flag = 0;
if (lbound[pos] > left && rlim[lbound[pos] - 1] <= rbound[pos])
{
flag = 1;
int now = --lbound[pos];
dfs(now, 0, left - 1);
lbound[pos] = min(lbound[pos], lbound[now]);
rbound[pos] = max(rbound[pos], rbound[now]);
}
if (rbound[pos] < right && llim[rbound[pos]] >= lbound[pos])
{
flag = 1;
int now = ++rbound[pos];
dfs(now, right + 1, n - 1);
lbound[pos] = min(lbound[pos], lbound[now]);
rbound[pos] = max(rbound[pos], rbound[now]);
}
}
};
for (int i = 0; i < n;i++)
dfs(i, 0, n - 1);
while (q--)
{
scanf("%d%d", &x, &y);
x--;
y--;
if (lbound[x] <= y && y <= rbound[x])
printf("Yes\n");
else
printf("No\n");
}
}
return 0;
}
题意:给定一个长度为 n n n 的排列 { p i } \{p_i\} {pi},任意两个点 ( i , j ) (i,j) (i,j) 的代价为之间有一条代价为 ∣ i − j ∣ ( p i − p j ) |i-j|(p_i-p_j) ∣i−j∣(pi−pj) 的边,问这 n n n 个点的最小生成树权值。 n ≤ 5 × 1 0 4 n\leq 5\times 10^4 n≤5×104。
解法:由 Kruskal 可以得到,显然可以通过相邻连接使得图完全连通。因而最大边的边权不会超过 n n n,因而 ∣ i − j ∣ |i-j| ∣i−j∣ 与 ∣ p i − p j ∣ |p_i-p_j| ∣pi−pj∣ 中必然有一个是小于等于 n \sqrt n n 的。暴力枚举 ∣ i − j ∣ ∈ [ 1 , n ] |i-j| \in [1, \sqrt n] ∣i−j∣∈[1,n] 和 ∣ p i − p j ∣ ∈ [ 1 , n ] |p_i-p_j| \in [1,\sqrt n] ∣pi−pj∣∈[1,n] 即可。对边使用桶排序,总时间复杂度 O ( n n ) \mathcal O(n \sqrt n) O(nn)。
#include
using namespace std;
const int N = 50000;
int pos[N + 5], a[N + 5];
vector<pair<int, int>> que[N + 5];
int father[N + 5];
int getfather(int x)
{
return father[x] == x ? x : father[x] = getfather(father[x]);
}
int main()
{
int t, n;
scanf("%d", &t);
while(t--)
{
scanf("%d", &n);
for (int i = 1; i <= n;i++)
father[i] = i;
int m = sqrt(n) + 1;
for (int i = 1; i <= n;i++)
{
scanf("%d", &a[i]);
pos[a[i]] = i;
}
long long ans = 0;
for (int i = 1; i <= m; i++)
for (int j = 1; i + j <= n;j++)
{
int val = i * abs(a[i + j] - a[j]);
if(val < n)
que[val].emplace_back(i + j, j);
}
for (int i = 1; i <= m; i++)
for (int j = 1; i + j <= n;j++)
{
int val = i * abs(pos[i + j] - pos[j]);
if(val < n)
que[val].emplace_back(pos[i + j], pos[j]);
}
int add = 0;
for (int i = 1; i <= n && add < n - 1; i++)
for (auto j : que[i])
if (getfather(j.first) != getfather(j.second))
{
father[getfather(j.first)] = getfather(j.second);
ans += i;
add++;
if(add == n - 1)
break;
}
printf("%lld\n", ans);
for (int i = 1; i <= n; i++)
que[i].clear();
}
return 0;
}
题意:给定一个 n n n 个点的树,从中选出一些点,使得其导出子图中每个点的度不超过 1 1 1,问最多可以选择几个点。 n ≤ 5 × 1 0 5 n \leq 5\times 10^5 n≤5×105。
解法:考虑树形 dp。
f
i
,
0
/
1
/
2
f_{i,0/1/2}
fi,0/1/2 表示当前
i
i
i 节点不选、选了且度为
0
0
0、选了且度为
1
1
1 的子树
i
i
i 最大选点数目。则有如下转移:
f
u
,
0
←
∑
v
∈
c
h
u
max
(
f
v
,
0
,
f
v
,
1
,
f
v
,
2
)
f
u
,
1
←
1
+
∑
v
∈
c
h
u
f
v
,
0
f
u
,
2
←
1
+
max
v
′
{
f
v
′
,
1
+
∑
v
∈
c
h
u
,
v
≠
v
′
f
v
,
0
}
若当前节点不选,则子树可以乱选;若当前节点选,且度为
0
0
0(即不和子树连接),则子树只能选择都不选;否则就可以选择一个子树去连接,其余的均不选。总时间复杂度
O
(
n
)
\mathcal O(n)
O(n)。
#include
using namespace std;
const int N = 500000;
struct line
{
int from;
int to;
int next;
};
struct line que[2 * N + 5];
int cnt, headers[N + 5];
void add(int from, int to)
{
cnt++;
que[cnt].from = from;
que[cnt].to = to;
que[cnt].next = headers[from];
headers[from] = cnt;
}
int f[N + 5][3];
void dfs(int place, int father)
{
f[place][0] = 0;
f[place][1] = f[place][2] = 1;
for (int i = headers[place]; i; i = que[i].next)
if (que[i].to != father)
{
dfs(que[i].to, place);
f[place][0] += max({f[que[i].to][0], f[que[i].to][1], f[que[i].to][2]});
f[place][1] += f[que[i].to][0];
}
for (int i = headers[place]; i; i = que[i].next)
if (que[i].to != father)
f[place][2] = max(f[place][2], f[place][1] - f[que[i].to][0] + f[que[i].to][1]);
}
int main()
{
int size(512 << 20); // 512M
__asm__ ( "movq %0, %%rsp\n"::"r"((char*)malloc(size)+size));
int t, n;
scanf("%d", &t);
while(t--)
{
scanf("%d", &n);
for (int i = 1, u, v; i < n;i++)
{
scanf("%d%d", &u, &v);
add(u, v);
add(v, u);
}
dfs(1, 1);
printf("%d\n", max({f[1][0], f[1][1], f[1][2]}));
cnt = 0;
for (int i = 1; i <= n;i++)
headers[i] = 0;
}
exit(0);
}
题意:给定一个 n n n 个点,以 1 1 1 为根的树,按照顺序依次进行 q q q 次操作:从某个点 x i x_i xi 开始,一路走到 1 1 1,将其路径上所有的边的颜色全部染成 c i c_i ci,并且与这条路径相连的其余所有的边的颜色全部清零。每个操作有 p i p_i pi 的概率成功,彼此独立,问最终所有边上颜色之和的期望。 n , q ≤ 2 × 1 0 5 n, q\leq 2\times 10^5 n,q≤2×105。
解法:考虑每条边对答案的贡献。从后到前的维护每个操作,考虑一条边是否会被最终留下来:
对于边 ( u , v ) (u,v) (u,v),它如果想要留下来,当且仅当需要经过 u → w u \to w u→w 的染色全部失败——从 v 1 → u v_1 \to u v1→u 的必然经过 u → w u \to w u→w;从 s 1 → v s_1 \to v s1→v 再上去的也得经过 u → w u \to w u→w;从 u u u 出发的 u → w u \to w u→w 也是经过 u → w u \to w u→w。因而每条边是否取到仅限制于其父亲往根节点走的那条边。若 u u u 为根节点,则 ( u , v ) (u,v) (u,v) 的颜色为 c i c_i ci 当且仅当后面所有的染色全部失败。
显然一次操作对应的若干条边的贡献可以通过一次树链剖分一并统计。考虑每条边用它的儿子来代表,对于根节点也建立一条虚边指向虚点 0 0 0。则问题转化为链上求和、链上乘法,使用树链剖分则可以在 O ( n log 2 n ) \mathcal O(n \log^2n) O(nlog2n) 的复杂度内通过。
#include
using namespace std;
const int N = 200000;
const long long mod = 1000000007;
long long power(long long a, long long x)
{
long long ans = 1;
while(x)
{
if(x&1)
ans = ans * a % mod;
a = a * a % mod;
x >>= 1;
}
return ans;
}
long long inv(long long a)
{
return power(a, mod - 2);
}
class segment_tree
{
struct node
{
long long sum;
long long tag;
node()
{
tag = 1;
sum = 0;
}
};
int n;
node t[4 * N + 5];
void pushdown(int place, int left, int right)
{
if (t[place].tag != 1)
{
int mid = (left + right) >> 1;
update(place << 1, left, mid, left, mid, t[place].tag);
update(place << 1 | 1, mid + 1, right, mid + 1, right, t[place].tag);
t[place].tag = 1;
}
}
void update(int place, int left, int right, int start, int end, long long x)
{
if (start <= left && right <= end)
{
t[place].tag = t[place].tag * x % mod;
t[place].sum = t[place].sum * x % mod;
return;
}
pushdown(place, left, right);
int mid = (left + right) >> 1;
if (start <= mid)
update(place << 1, left, mid, start, end, x);
if (end > mid)
update(place << 1 | 1, mid + 1, right, start, end, x);
t[place].sum = (t[place << 1].sum + t[place << 1 | 1].sum) % mod;
}
long long query(int place, int left, int right, int start, int end)
{
if (start <= left && right <= end)
return t[place].sum;
pushdown(place, left, right);
int mid = (left + right) >> 1;
long long ans = 0;
if (start <= mid)
ans = (ans + query(place << 1, left, mid, start, end));
if (end > mid)
ans = (ans + query(place << 1 | 1, mid + 1, right, start, end));
return ans;
}
void build(int place, int left, int right)
{
t[place].tag = 1;
t[place].sum = 1;
if (left == right)
return;
int mid = (left + right) >> 1;
build(place << 1, left, mid);
build(place << 1 | 1, mid + 1, right);
t[place].sum = (t[place << 1].sum + t[place << 1 | 1].sum) % mod;
}
public:
void build(int n)
{
this->n = n;
build(1, 1, n);
}
void update(int left, int right, long long x)
{
update(1, 1, n, left, right, x);
}
long long query(int left, int right)
{
return query(1, 1, n, left, right);
}
} t;
struct line
{
int from;
int to;
int next;
};
struct line que[2 * N + 5];
int cnt, headers[N + 5];
void add(int from, int to)
{
cnt++;
que[cnt].from = from;
que[cnt].to = to;
que[cnt].next = headers[from];
headers[from] = cnt;
}
int father[N + 5], siz[N + 5], maxson[N + 5];
void dfs1(int place, int fa)
{
father[place] = fa;
siz[place] = 1;
maxson[place] = 0;
for (int i = headers[place]; i; i = que[i].next)
if (que[i].to != fa)
{
dfs1(que[i].to, place);
siz[place] += siz[que[i].to];
if(!maxson[place] || siz[maxson[place]] < siz[que[i].to])
maxson[place] = que[i].to;
}
}
int tp[N + 5], id[N + 5], ind;
void dfs2(int place, int ance)
{
tp[place] = ance;
id[place] = ++ind;
if(maxson[place])
dfs2(maxson[place], ance);
for (int i = headers[place]; i; i = que[i].next)
if (que[i].to != maxson[place] && que[i].to != father[place])
dfs2(que[i].to, que[i].to);
}
void update(int u, long long p)
{
while (u)
{
t.update(id[tp[u]], id[u], p);
u = father[tp[u]];
}
}
long long query(int u)
{
if(u == 1)
return 0;
u = father[u];
long long ans = 0;
while (u)
{
ans = (ans + t.query(id[tp[u]], id[u])) % mod;
u = father[tp[u]];
}
return ans;
}
struct operation
{
int id, c;
long long p;
} op[N + 5];
int main()
{
int caset, n, q;
scanf("%d", &caset);
while(caset--)
{
scanf("%d%d", &n, &q);
for (int i = 2, u; i <= n;i++)
{
scanf("%d", &u);
add(i, u);
add(u, i);
}
dfs1(1, 0);
dfs2(1, 0);
t.build(ind);
for (int i = 1; i <= q;i++)
scanf("%d%d%lld", &op[i].id, &op[i].c, &op[i].p);
long long ans = 0;
for (int i = q; i >= 1;i--)
{
ans = (ans + op[i].p * query(op[i].id) % mod * op[i].c % mod) % mod;
update(op[i].id, mod + 1 - op[i].p);
}
printf("%lld\n", ans);
ind = cnt = 0;
for (int i = 1; i <= n;i++)
headers[i] = 0;
}
return 0;
}
题意:平面上初始有 n n n 个点 ( x i , y i ) (x_i,y_i) (xi,yi)。 t t t 时刻平面上初始的 n n n 个点会变成 4 n 4n 4n 个点—— ( x i + t , y i ) (x_i+t,y_i) (xi+t,yi), ( x i − t , y i ) (x_i-t,y_i) (xi−t,yi), ( x i , y i − t ) (x_i,y_i-t) (xi,yi−t), ( x i , y i + t ) (x_i,y_i+t) (xi,yi+t)。 q q q 次询问 t t t 时刻这 4 n 4n 4n 个点围成的凸包面积。 n , q ≤ 1 × 1 0 5 n,q \leq 1\times 10^5 n,q≤1×105,坐标范围 1 × 1 0 9 1\times 10^9 1×109。
解法:首先对这 n n n 个点取凸包 C m C_m Cm。考虑 t t t 时刻的凸包:
容易发现周围一定有一个完整的边长为 2 t \sqrt 2t 2t 的正方形,并且每条边向外扩展了 t t t。对于一条边 A B AB AB,若斜率的绝对值超过 1 1 1,则其扩展面积为 ∣ y A − y B ∣ t |y_A-y_B|t ∣yA−yB∣t;否则为 ∣ x A − x B ∣ t |x_A-x_B|t ∣xA−xB∣t。因而扩展面积为 t ∑ i = 1 m max ( ∣ x i − x i + 1 ∣ , ∣ y i − y i + 1 ∣ ) \displaystyle t\sum_{i=1}^m \max(|x_i-x_{i+1}|,|y_i-y_{i+1}|) ti=1∑mmax(∣xi−xi+1∣,∣yi−yi+1∣)。因而 t t t 时刻的答案为 2 t 2 + t ∑ i = 1 m max ( ∣ x i − x i + 1 ∣ , ∣ y i − y i + 1 ∣ ) + S \displaystyle 2t^2+t\sum_{i=1}^m \max(|x_i-x_{i+1}|,|y_i-y_{i+1}|)+S 2t2+ti=1∑mmax(∣xi−xi+1∣,∣yi−yi+1∣)+S,其中 S S S 为初始面积。由于给定均为整点,因而需要使用整型,并且最后手动处理小数点(至多只有一位小数)。
int main()
{
int caset, n, q;
long long t;
scanf("%d", &caset);
while(caset--)
{
scanf("%d%d", &n, &q);
vector<Point> que(n);
for (int i = 0; i < n;i++)
scanf("%lld%lld", &que[i].x, &que[i].y);
Convex c = convexhull(que);
long long doubleS = c.area();
long long C = 0;
int n = c.p.size();
auto nx = [&](int x)
{
return (x + 1) % n;
};
for (int i = 0; i < n; i++)
C += max(abs(c.p[i].x - c.p[nx(i)].x), abs(c.p[i].y - c.p[nx(i)].y));
while (q--)
{
scanf("%lld", &t);
long long ans = t * C + 2 * t * t + doubleS / 2;
printf("%lld", ans);
if(doubleS & 1)
printf(".5\n");
else
printf(".0\n");
}
}
return 0;
}
题意:给定 n × m n \times m n×m 的矩形,横平竖直的切分尽可能多刀使得每块子矩形的面积均大于等于 k k k,问最多切多少刀。 n , m , k ≤ 1 × 1 0 5 n,m,k \leq 1\times 10^5 n,m,k≤1×105。
解法:枚举子矩形的长 x x x,求出最小的子矩形的宽 y = ⌈ k x ⌉ y=\left \lceil \dfrac{k}{x}\right \rceil y=⌈xk⌉,求 max 1 ≤ x ≤ k { ⌊ ( n x ⌋ , ⌊ m y ⌋ } − 2 \displaystyle \max_{1 \leq x\leq k}\left \{\left \lfloor \dfrac{\mathstrut n}{x}\right \rfloor,\left \lfloor \dfrac{m}{y}\right \rfloor\right \}-2 1≤x≤kmax{⌊x(n⌋,⌊ym⌋}−2,必须保证 x ≤ n x \leq n x≤n 且 y ≤ m y \leq m y≤m。时间复杂度 O ( k ) \mathcal O(k) O(k)。
题意:求长度为 m m m,序列中每个元素属于 [ 1 , n ] [1,n] [1,n],且任意连续 n n n 个数字不能是长度为 n n n 的排列的序列个数。 n , m ≤ 2 × 1 0 5 n,m \leq 2\times 10^5 n,m≤2×105。
解法:考虑
f
i
f_i
fi 表示在
i
i
i 处开头出现一个长度为
n
n
n 的排列,且前
i
−
1
i-1
i−1 个数字构不成一个排列的方案数。有转移:
f
i
=
n
i
−
1
n
!
−
∑
j
=
1
i
−
n
f
j
(
n
i
−
j
−
n
n
!
)
−
∑
j
=
i
−
n
+
1
i
−
1
f
j
(
i
−
j
)
!
f_i=n^{i-1}n!-\sum_{j=1}^{i-n}f_j(n^{i-j-n}n!)-\sum_{j=i-n+1}^{i-1}f_j(i-j)!
fi=ni−1n!−j=1∑i−nfj(ni−j−nn!)−j=i−n+1∑i−1fj(i−j)!
即,全集
n
i
−
1
n
!
n^{i-1}n!
ni−1n! 减去
[
1
,
i
−
1
]
[1,i-1]
[1,i−1] 包含一个完整排列的方案,减去上一个排列和当前排列有交集的方案。则很容易的利用分治 NTT 优化到
O
(
n
log
2
n
)
\mathcal O(n \log^2n)
O(nlog2n),在实际测评中其效率与多项式求逆的效率差不多。
void Solve() {
scanf("%d%d", &n, &m);
int w = POW(n, m - n, fac[n]);
Poly g(m + 1);
g[1] = POW(n);
fp(i, 2, n) g[i] = mul(g[i - 1], g[1]);
fp(i, 2, n) g[i] = mul(g[i], fac[i]);
fp(i, n + 1, m) g[i] = g[n];
Poly h = (g >> 1).semiConv({w}, m - n + 1, [&](Poly &f, int m) { f[m] = (w - f[m] + P) % P; });
ll ans = POW(n, m);
fp(i, 0, m - n) ans += P - h[i];
printf("%lld\n", ans % P);
}