7.4 Thu.
杂题选讲
出处:
A.
依次考虑每个人
起点到终点构成一个矩形,矩形内有关键点的话答案就是 两点间 d i s dis dis
基于曼哈顿距离的特殊性,将整张图分成九个部分,每个部分内的问题是相似的
第一想法是对每个矩形内部维护相应的关键点的 值 ( 比如
x
+
y
,
x
−
y
x+y,x-y
x+y,x−y 等 ) 的
M
a
x
/
M
i
n
Max / Min
Max/Min
可能需要上高级数据结构
注意到 x , y x,y x,y 只有 1000 1000 1000 ,那么仅枚举 9 9 9 个部分在带上 l o g log log 查询的复杂度显得有点多余了
更好的做法是枚举每一列,实际上每一列有用的点最多只有三个(离矩形上下边界最近的)
这个东西可以直接 O ( V 2 ) O(V^2) O(V2) 在原地图上递推得到
int mp[M][M] , up[M][M] , dn[M][M] ; // 每个点上/下第一个关键点
void pre_work()
{
for(int j = 1 ; j <= 1000 ; j ++ ) {
up[0][j] = -1e9 ;
for(int i = 1 ; i <= 1000 ; i ++ ) {
if( mp[i][j] ) up[i][j] = i ;
else up[i][j] = up[i-1][j] ;
}
dn[1001][j] = 1e9 ;
for(int i = 1000 ; i >= 1 ; i -- ) {
if( mp[i][j] ) dn[i][j] = i ;
else dn[i][j] = dn[i+1][j] ;
}
}
}
void solve( int sx , int sy , int tx , int ty ) // 从左到右
{
int mn = min( sx , tx ) , mx = max( sx , tx ) ;
int ans = abs(sx-tx) + abs(sy-ty) , ext = 1e9 ;
for(int j = 1 ; j <= 1000 ; j ++ ) {
if( j < sy ) {
ext = min( ext , 2*(sy-j+mn-up[mn][j]) ) ;
ext = min( ext , 2*(sy-j+dn[mx][j]-mx) ) ;
if( dn[mn][j] <= mx ) {
ext = min( ext , 2*(sy-j) ) ;
}
}
else if( j <= ty ) {
ext = min( ext , 2*(mn-up[mn][j]) ) ;
ext = min( ext , 2*(dn[mx][j]-mx) ) ;
if( dn[mn][j] <= mx ) {
ext = 0 ;
break ;
}
}
else {
ext = min( ext , 2*(j-ty+mn-up[mn][j]) ) ;
ext = min( ext , 2*(j-ty+dn[mx][j]-mx) ) ;
if( dn[mn][j] <= mx ) {
ext = min( ext , 2*(j-ty) ) ;
}
}
}
printf("%d\n" , ext+ans ) ;
}
*B.
好题
线段树好像很难处理这种东西(带修主席树?),第一想法是分块
发现问题在于 合并两个整块时 颜色信息 无法快速维护,这个信息不具有 “可加性”
没办法了,考虑莫队,动态维护询问区间,让左右端点 + 1 , − 1 +1 ,-1 +1,−1
还有一个出发点是 这个题给了 询问区间不交 的性质,莫队的指针移动就是 O ( n ) O(n) O(n) 的
莫队的劣势在于 本题带修,需要考虑操作顺序的问题,解决办法是 动态维护时间戳
发现对于当前维护的区间来说,每种颜色可以 只用时间戳最小的来代表
维护 n n n 个 s e t set set 表示 n n n 种颜色的时间戳集合 ,每个 s e t set set 取 b e g i n begin begin 加到树状数组里,查询时只需查时间戳前缀的颜色数
左右端点移动时 先删掉对应 s e t set set 的 b e g i n begin begin 在树状数组中的贡献,修改完 s e t set set 后再补上新 b e g i n begin begin 的贡献
int n , m ;
vector<int> L[N] , R[N] ;
int l1 , l2 ;
struct nn
{
int l , r , c , tim ;
}op[N] ;
struct qe
{
int l , r , tim , id ;
}q[N] ;
bool cmp ( qe x , qe y )
{
return x.l < y.l ;
}
int lx , rx , ans[N] ;
set<int> s[N] ;
struct BIT
{
int t[N] ;
inline int lowbit( int x ) { return x&-x ; }
void add( int p , int x ) { for( ; p <= n ; p += lowbit(p) ) t[p] += x ; }
int ask( int p ) {
int res = 0 ;
for( ; p ; p -= lowbit(p) ) res += t[p] ;
return res ;
}
}t ;
void Insert( int c , int tim ) // 添加 x 操作
{
if( !s[c].empty() ) {
int tm = *s[c].begin() ;
t.add( tm , -1 ) ;
}
s[c].insert( tim ) ;
int tm = *s[c].begin() ;
t.add( tm , 1 ) ;
}
void Erase( int c , int tim )
{
int tm = *s[c].begin() ;
t.add( tm , -1 ) ;
s[c].erase( tim ) ;
if( !s[c].empty() ) {
int tm = *s[c].begin() ;
t.add( tm , 1 ) ;
}
}
int main()
{
scanf("%d%d" , &n , &m ) ;
int opt ;
for(int i = 1 ; i <= m ; i ++ ) {
scanf("%d" , &opt ) ;
if( opt == 1 ) {
l1 ++ ;
scanf("%d%d%d" , &op[l1].l , &op[l1].r , &op[l1].c ) ;
op[l1].tim = i ;
L[op[l1].l].push_back( l1 ) ;
R[op[l1].r].push_back( l1 ) ;
}
else {
l2 ++ ;
scanf("%d%d" , &q[l2].l , &q[l2].r ) ;
q[l2].tim = i ;
q[l2].id = l2 ;
}
}
sort( q+1 , q+l2+1 , cmp ) ;
}
复杂度是 O ( n l o g ) O(nlog) O(nlog) 的
C.
1
0
4
10^4
104 个点放到
50
50
50 条直线上?
直接随机化,每次随机找两个点求出斜率作为 K K K, c h e c k check check
c h e c k check check 过程:
对于每个点,找到经过它斜率为 K K K 的直线截距 B B B,只需统计 B B B 的种类即可
需要注意 B B B 是 d o u b l e double double 的话 m a p map map 是没法有效去重的,要对等式变形,去分母搞成整数做
D.
式子很奇怪,进行一步常用变形:
{ a i − i ≤ a j − j i − b i ≤ j − b j ⟹ { X i ≤ X j Y i ≤ Y j \begin{cases} a_i-i\leq a_j-j\\ i-b_i\leq j-b_j \end{cases} \ \ \ \ \Longrightarrow\ \ \ \ \ \begin{cases} X_i\leq X_j\\ Y_i\leq Y_j \end{cases} {ai−i≤aj−ji−bi≤j−bj ⟹ {Xi≤XjYi≤Yj
其实是一个二维偏序
首先按 X X X 排序,维护一个单减栈:
一个 Y i Y_i Yi 可以连通其左下部分的所有点,每加入一个 Y i Y_i Yi 可以把栈内所有小于它的点连通,这些点先都出栈
那么对于已加入的点来说, Y i Y_i Yi 显然越小越好(越容易被后面连上)
那么一个连通块就只用其 Y i Y_i Yi 最小的那个来代表,把出栈的点中最小的塞回去,就 ok 了
int n ;
struct nn
{
int x , y ;
}a[N] ;
bool cmp ( nn x , nn y )
{
return x.x < y.x || (x.x==y.x&&x.y<y.y) ;
}
int st[N] , top ;
int main()
{
scanf("%d" , &n ) ;
int b , c ;
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%d%d" , &b , &c ) ;
a[i].x = b-i ;
a[i].y = i-c ;
}
sort( a+1 , a+n+1 , cmp ) ;
for(int i = 1 ; i <= n ; i ++ ) {
int Min = a[i].y ;
while( top && a[i].y >= st[top] ) {
Min = min( Min , st[top] ) ;
top -- ;
}
st[++top] = Min ;
}
printf("%d" , top ) ;
return 0 ;
}
巧妙的单调栈用法
E.
树形 DP ,发现儿子子树
s
i
z
e
size
size 的奇偶性会影响走出来时的状态,按这个分讨一下
*F.
好题
看到最大距离最小先考虑二分答案
二分了当前最大距离 m i d mid mid ,考虑怎么 c h e c k check check
整体思路是 枚举每个点对,标记出能作为根的节点,最后对所有标记取交
首先 对于点对 ( x , y ) (x,y) (x,y) ,怎么找能作为根的节点
一个比较显然的事情:两个点的 LCA 不管怎么选根,一定在它们之间的路径上
继续拆:能让 x x x 合法的根 与 能让 y y y 合法的根,分别标记再取交
不妨考虑 x x x ,手玩一下就知道怎么做了
这里第二种情况是用了一个 全局增加 + 补集减少 的 trick,因为补集是一棵完整子树,dfs 序上差分即可
int n , m ;
vector<int> E[N] ;
int dep[N] , fat[N][22] , dfn[N] , tim , ed[N] ;
void dfs( int x , int fa )
{
dfn[x] = ++tim ;
dep[x] = dep[fa] + 1 ; fat[x][0] = fa ;
for(int i = 1 ; i <= 20 ; i ++ ) fat[x][i] = fat[fat[x][i-1]][i-1] ;
for(int t : E[x] ) {
if( t == fa ) continue ;
dfs( t , x ) ;
}
ed[x] = tim ;
}
int LCA( int x , int y )
{
if( dep[x] < dep[y] ) swap( x , y ) ;
for(int i = 20 ; i >= 0 ; i -- ) {
if( dep[fat[x][i]] >= dep[y] ) x = fat[x][i] ;
}
if( x == y ) return x ;
for(int i = 20 ; i >= 0 ; i -- ) {
if( fat[x][i] != fat[y][i] ) x = fat[x][i] , y = fat[y][i] ;
}
return fat[x][0] ;
}
int Get( int x , int p )
{
for(int i = 20 ; i >= 0 ; i -- ) {
if( (p>>i)&1 ) {
x = fat[x][i] ;
}
}
return x ;
}
int X[N] , Y[N] , ADD , c[N] ;
inline void add( int l , int r , int d )
{
c[l] += d , c[r+1] -= d ;
}
bool check( int mid )
{
ADD = 0 ;
for(int i = 0 ; i <= n+1 ; i ++ ) c[i] = 0 ;
for(int i = 1 ; i <= m ; i ++ ) {
int x = X[i] , y = Y[i] , lca = LCA( x , y ) ;
int dis = dep[x]+dep[y]-2*dep[lca] ;
if( dis <= mid ) {
ADD += 2 ;
continue ;
}
if( dep[x] - dep[lca] > mid ) {
int p = Get( x , mid ) ;
add( dfn[p] , ed[p] , 1 ) ;
}
else {
ADD ++ ;
int p = Get( y , dis-mid-1 ) ;
add( dfn[p] , ed[p] , -1 ) ;
}
if( dep[y] - dep[lca] > mid ) {
int p = Get( y , mid ) ;
add( dfn[p] , ed[p] , 1 ) ;
}
else {
ADD ++ ;
int p = Get( x , dis-mid-1 ) ;
add( dfn[p] , ed[p] , -1 ) ;
}
}
for(int i = 1 ; i <= n ; i ++ ) {
c[i] += c[i-1] ;
if( c[i]+ADD == 2*m ) {
return 1 ;
}
}
return 0 ;
}
*G.
讨厌的数数题
对于最终一个合法的序列,考虑一下怎么去找子问题
结合那个性质,当前末尾位置往前 K K K 个必定是一个排列,以此作为状态
转移就往前枚举一个位置 j j j 作为上一个排列的结尾,为避免重复,钦定 j j j 是最靠右的能构成排列的位置,也就是说, [ j + 1 , i − 1 ] [j+1,i-1] [j+1,i−1] 任意一个位置作为结尾都不能是 1 ∼ K 1\sim K 1∼K 的排列
( 这也是计数 dp 的核心 )
为了要跟 j j j 前面构成排列,此时 [ j + 1 , i ] [j+1,i] [j+1,i] 要填哪些数字是固定的,只是方案不定,那么:
f [ i ] ← f [ j ] × ( i − j ) ! f[i]\leftarrow f[j]\times(i-j)! f[i]←f[j]×(i−j)!
这样我们就会发现答案大了!
为什么呢?注意我们上面钦定的那一条
如果单纯 ( i − j ) ! (i-j)! (i−j)! 的话可能会使 [ j + 1 , i − 1 ] [j+1,i-1] [j+1,i−1] 出现新的能构成排列的位置
设这个系数为 g [ i ] g[i] g[i] ,表示在 1 ∼ K 1\sim K 1∼K 的排列后接上 i i i 个数, i i i 能作为右端点构成排列,且 [ K + 1 , K + i − 1 ] [K+1,K+i-1] [K+1,K+i−1] 作为右端点都 不能 构成 排列,填这 i i i 个数的方案
首先 g [ i ] = i ! g[i]=i! g[i]=i!
接下来考虑容斥:
从 j + 1 j+1 j+1 开始枚举到 i − 1 i-1 i−1 ,钦定当前枚举的 x x x 是从左往右第一个能构成排列的,也就是 [ j + 1 , x − 1 ] [j+1,x-1] [j+1,x−1] 任意一个位置都不能构成排列
发现问题递归了,这就是 g [ x − j ] g[x-j] g[x−j] ,后面 [ x + 1 , i ] [x+1,i] [x+1,i] 就随便填了,那么 g g g 就可以求, f f f 也就能求了
int n , K ;
LL f[N] , fac[1010] , g[1010] ;// 前 i 位置,钦定 i 包含在 以i结尾的 1~k 的排列中,方案数
int main()
{
scanf("%d%d" , &n , &K ) ;
fac[0] = 1 ;
for(int i = 1 ; i <= K ; i ++ ) fac[i] = fac[i-1] * i % mod ;
f[K] = fac[K] ;
for(int i = 1 ; i <= K ; i ++ ) {
g[i] = fac[i] ;
for(int j = 1 ; j < i ; j ++ ) g[i] = ( g[i] - fac[j]*g[i-j]%mod + mod ) % mod ;
}
for(int i = K+1 ; i <= n ; i ++ ) {
// 用 从i往前找到的第一个 p 使得 [p-k+1,p] 为 1~k排列 来划分
for(int j = 1 ; j <= K ; j ++ ) {
f[i] = ( f[i] + f[i-j] * g[j] % mod ) % mod ; // 为避免重复,需要处理一个系数数组 g
}
}
printf("%lld" , f[n] ) ;
return 0 ;
}
H.
非常神奇的一道题
首先按 b i b_i bi 分层,每层内可以按 L L L 排序
那么题目中的限制转化为:
同一层内不能有相互包含的 , 相邻层 之间不能下层不能包含上层
这里这考虑相邻层即可,假设 i i i 被 j ( b i + 1 < b j ) j\ \ (b_i+1<b_j) j (bi+1<bj) 包含,由于 b j − 1 b_j-1 bj−1 层中必有一个包含 j j j ,同理往上推,会得到 b i + 1 b_i+1 bi+1 层中必有一个包含 i i i
那么就只需要保证相邻层之间 下层每个都被上层某个包含,同一层内 随 L L L 单增, R R R 单增(若同层有 L L L 相同,显然不合法了)
越靠上的层显然越大越好,那么就是贪心的尽可能把右端点往右放
注意每层右端点单增的限制,所以要从大到小枚举
L
L
L ,维护当前最小右端点
n
o
w
now
now,找到一个新的右端点时要与
n
o
w
−
1
now-1
now−1 取
M
i
n
Min
Min
int n ;
vector<int> ve[M] ;
int l[N] , r[N] ;
bool cmp( int x , int y )
{
return l[x] < l[y] ;
}
int main()
{
scanf("%d" , &n ) ;
int b , Max = 0 ;
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%d%d" , &l[i] , &b ) ;
Max = max( Max , b ) ;
ve[b].push_back( i ) ;
} // 对每一层分开做
for(int i = 0 ; i <= Max ; i ++ ) {
if( ve[i].size() == 0 ) {
printf("-1\n") ;
return 0 ;
}// 断档
sort( ve[i].begin() , ve[i].end() , cmp ) ;
if( i == 0 ) {
int now = 1e6 ;
for(int j = ve[i].size()-1 ; j >= 0 ; j -- ) {
int id = ve[i][j] ;
if( j && l[id] == l[ve[i][j-1]] ) {
printf("-1\n") ;
return 0 ;
}
r[id] = now ;
now -- ;
}
}
else {
int now = 1e6+1 ;
for(int j = ve[i].size()-1 , p = ve[i-1].size()-1 ; j >= 0 ; j -- ) {
if( j && l[ve[i][j]] == l[ve[i][j-1]] ) {
printf("-1\n") ;
return 0 ;
} // 同层有相同 L
while( p-1 >= 0 && l[ve[i-1][p]] > l[ve[i][j]] ) {
p -- ;
}// 找到最接近的上一层的左端点
if( l[ve[i-1][p]] <= l[ve[i][j]] ) {
if( l[ve[i-1][p]] < l[ve[i][j]] ) {
r[ve[i][j]] = min( now-1 , r[ve[i-1][p]] ) ;
now = r[ve[i][j]] ;
}
else {
r[ve[i][j]] = min( now-1 , r[ve[i-1][p]]-1 ) ;
now = r[ve[i][j]] ;
}
}
else {
printf("-1\n") ;
return 0 ;
}
}
}
}
for(int i = 1 ; i <= n ; i ++ ) {
if( l[i] > r[i] ) {
printf("-1\n") ;
return 0 ;
}
}
for(int i = 1 ; i <= n ; i ++ ) {
printf("%d\n" , r[i] ) ;
}
return 0 ;
}
I.
经过反复手玩,会发现
1 1 1 操作的相对顺序是固定的,且只针对最终序列中形如 ( ) () () 的,只能从左往右放
对于 2 2 2 操作,实际上唯一的限制只有 必须在某个 1 1 1 操作之后进行
那么倒着做,每个 ( ) () () 相当于进行了一次 将新的 2 2 2 操作生成的括号 插入到原序列中,再把这个 ( ) () () 对应的 1 1 1 操作放到开头,那么这就是一个常规的组合问题
由于 “选择不同区间视为不同 2 2 2 操作”,在插入一段 2 2 2 操作时会有顺序问题,需要乘上排列
int main()
{
scanf("%s" , ch+1 ) ;
n = strlen( ch+1 ) ;
maxn = n ;
pre_work() ;
for(int i = 1 ; i < n ; i ++ ) {
if( ch[i] == '(' && ch[i+1] == ')' ) {
cnt ++ ;
i ++ ;
while( i+1 <= n && ch[i+1] == ')' ) v[cnt] ++ , i ++ ;
}
}
LL ans = 1 ;
int S = 0 ;
for(int i = cnt ; i >= 1 ; i -- ) {
S += v[i] ;
ans = ans * C(S,v[i]) % mod * fac[v[i]] % mod ;
S ++ ;
}
printf("%lld" , ans ) ;
return 0 ;
}
*J
好题
每次询问只与一部分数有关,考虑对这些数的出现次数进行根号分治
1.若 c n t p < n cnt_p<\sqrt n cntp<n 且 c n t q < n cnt_q<\sqrt n cntq<n
直接暴力双指针即可,复杂度 O ( n ) O(\sqrt n) O(n)
2.若 其中一个出现次数大于等于 n \sqrt n n,不妨设为 p p p
这样的 p p p 只有 n \sqrt n n 个,称这些为 关键点
只有两种数的逆序对是好算的,等价于 对于每一个 q ∈ [ l , r ] q \in [l,r] q∈[l,r] 后面的 p ∈ [ l , r ] p\in [l,r] p∈[l,r] 出现次数 求和
显然等价于预处理每一个 q q q 后面 p p p 的出现次数,再滚前缀和,然后把 r r r 后面的减掉即可
由于 p p p 只有 n \sqrt n n 个,考虑开数组 t [ p ] [ i ] t[p][i] t[p][i] 表示 对于 关键点 p p p, i i i 位置对应的颜色中,出现 p p p 次数 到 i i i 的后缀和
本题卡空间,块长需要微调
#include<bits/stdc++.h>
using namespace std ;
typedef long long LL ;
const int N = 1e5 + 5 , B = 155 ;
int n , T , a[N] , cnt[N] , lst[N] , bl ;
int len , c[B] ;
LL s[B][N] , t[B][N] ; // 只维护 B种,对同颜色滚前缀和
vector<int> ps[N] ;
void sol1( int l , int r , int p , int q ) // sqrt n 双指针
{
int j = -1 , ans = 0 ;
while( j+1 < ps[q].size() && ps[q][j+1] < l ) j ++ ;
int st = j ;
for(int i = 0 ; i < ps[p].size() ; i ++ ) {
if( ps[p][i] < l ) continue ;
if( ps[p][i] > r ) break ;
while( j+1 < ps[q].size() && ps[q][j+1] <= ps[p][i] ) {
j ++ ;
}
ans += j-st ;
}
printf("%d\n" , ans ) ;
}
int nam[N] ;
void sol2( int l , int r , int p , int q )
{
if( cnt[p] == 0 || cnt[q] == 0 ) {
printf("0\n") ;
return ;
}
if( cnt[p] >= bl ) {
int ID = nam[p] ;
int p1 = lower_bound( ps[q].begin() , ps[q].end() , l ) - ps[q].begin() ;
int p2 = upper_bound( ps[q].begin() , ps[q].end() , r ) - ps[q].begin() ;
int p3 = lower_bound( ps[p].begin() , ps[p].end() , l ) - ps[p].begin() ;
p1 -- , p2 -- ;
int L = ps[q][p1] , R = ps[q][p2] ;
LL ans = s[ID][R] - s[ID][L] - 1LL*(p2-p1)*(p3-1) ;
printf("%lld\n" , ans ) ;
return ;
}
int ID = nam[q] ;
int p1 = lower_bound( ps[p].begin() , ps[p].end() , l ) - ps[p].begin() ;
int p2 = upper_bound( ps[p].begin() , ps[p].end() , r ) - ps[p].begin() ;
int p3 = upper_bound( ps[q].begin() , ps[q].end() , r ) - ps[q].begin() ;
int L = ps[p][p1] , R = ps[p][p2] ;
LL ans = t[ID][L] - t[ID][R] - 1LL*(p2-p1)*(ps[q].size()-p3-1) ;
printf("%lld\n" , ans ) ;
return ;
}
int main()
{
scanf("%d%d" , &n , &T ) ;
bl = max( n/151 , int(sqrt(n)) ) ;
for(int i = 1 ; i <= n ; i ++ ) ps[i].push_back( 0 ) ; // 占位
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%d" , &a[i] ) ;
ps[a[i]].push_back( i ) ;
cnt[a[i]] ++ ;
if( cnt[a[i]] == bl ) c[++len] = a[i] , nam[a[i]] = len ;
}
for(int i = 1 ; i <= n ; i ++ ) ps[i].push_back( n+1 ) ;// 占位
for(int i = 1 ; i <= len ; i ++ ) {
int T = 0 ;
memset( lst , 0 , sizeof lst ) ;
for(int j = 1 ; j <= n ; j ++ ) {
if( a[j] == c[i] ) T ++ ;
s[i][j] = s[i][lst[a[j]]] + T ;
lst[a[j]] = j ;
}
T = 0 ;
memset( lst , 0 , sizeof lst ) ;
for(int j = n ; j >= 1 ; j -- ) {
if( a[j] == c[i] ) T ++ ;
t[i][j] = t[i][lst[a[j]]] + T ;
lst[a[j]] = j ;
}
}
int l , r , p , q ;
for(int i = 1 ; i <= T ; i ++ ) {
scanf("%d%d%d%d" , &l , &r , &p , &q ) ;
if( cnt[p] < bl && cnt[q] < bl ) {
sol1( l , r , p , q ) ;
}
else {
swap( p , q ) ;
sol2( l , r , p , q ) ;
}
}
return 0 ;
}