Increasing Subsequence(期望dp)
求期望轮数。
期望dp都是推转移式
先给个暴力
O
(
n
3
)
O(n^3)
O(n3)的做法。
d
p
[
0
]
[
i
]
[
j
]
dp[0][i][j]
dp[0][i][j]表示当前是Alice选,面临局面是Alice之前选到
a
[
i
]
a[i]
a[i],Bob选到
a
[
j
]
a[j]
a[j]。
//
// Created by Artist on 2021/11/4.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
#define endl '\n'
const int maxn = 5003;
const int mod = 998244353;
int qpow(int aa,int n) {
int ans=1;
while(n) {
if(n&1) ans=1ll*aa*ans%mod;
aa=1ll*aa*aa%mod;
n>>=1;
}
return ans;
}
int a[maxn];
int dp[2][maxn][maxn];
int vis[2][maxn][maxn];
int big[maxn];
signed main() {
int n;scanf("%d",&n);
for(int i=1;i<=n;++i) {
scanf("%d",&a[i]);
}
for(int i=1;i<=n;++i) {
for(int j=1;j<=n;++j) {
for(int k=j+1;k<=n;++k) {
if(a[i]>a[j]&&a[k]>max(a[i],a[j])) vis[1][i][j]++;
}
for(int k=i+1;k<=n;++k) {
if(a[i]<a[j]&&a[k]>max(a[i],a[j])) vis[0][i][j]++;
}
}
}
for(int i=n;i;--i) {
for(int j=n;j;--j) {
if(vis[1][i][j]) {
for(int k=j+1;k<=n;++k) {
if(a[k]>a[i])
dp[1][i][j]=(dp[1][i][j]+1ll*qpow(vis[1][i][j],mod-2)*dp[0][i][k]%mod)%mod;
}
dp[1][i][j]=(dp[1][i][j]+1ll)%mod;
}
if(vis[0][i][j]) {
for(int k=i+1;k<=n;++k) {
if(a[k]>a[j])
dp[0][i][j]=(dp[0][i][j]+1ll*qpow(vis[0][i][j],mod-2)*dp[1][k][j]%mod)%mod;
}
dp[0][i][j]=(dp[0][i][j]+1ll)%mod;
}
}
}
int ans=0;
int n1 = qpow(n,mod-2);
for(int i=1;i<=n;++i) {
for(int j=1;j<=n;++j) if(a[j]>a[i]) big[i]++;
}
for(int i=1;i<=n;++i) {
// 不存在
if(big[i]==0) {
// 1/n的概率来到这个局面,轮数仅为1
ans=(ans+n1)%mod;
continue;
}
for(int j=1;j<=n;++j) {
if(a[j]>a[i]) {
// 1/n*1/big的概率来到这个局面,轮数为后续+2
ans=(ans+1ll*n1*qpow(big[i],mod-2)%mod*(dp[0][i][j]+2ll)%mod)%mod;
}
}
}
printf("%d\n",ans);
}
现考虑如何优化,压缩掉最后一个k的枚举。
因为k的枚举与之前j的枚举有重叠的地方。
考虑原来的dp:
第二个人走:
a
[
i
]
>
a
[
j
]
a[i]>a[j]
a[i]>a[j]
d
p
(
i
,
j
)
=
1
+
1
c
1
∑
d
p
(
i
,
k
1
)
dp(i,j)=1+\frac{1}{c_1}\sum dp(i,k_1)
dp(i,j)=1+c11∑dp(i,k1)
其中
c
1
c_1
c1为
j
j
j后面大于
a
[
i
]
a[i]
a[i]的数的个数。
k
1
k_1
k1满足
k
1
>
j
k_1>j
k1>j且
a
[
k
1
]
>
a
[
i
]
a[k_1]>a[i]
a[k1]>a[i]
第一个人走:
a
[
j
]
>
a
[
i
]
a[j]>a[i]
a[j]>a[i]
d
p
(
i
,
j
)
=
1
+
1
c
2
∑
d
p
(
k
2
,
j
)
dp(i,j)=1+\frac{1}{c_2}\sum dp(k_2,j)
dp(i,j)=1+c21∑dp(k2,j)
其中
c
2
c_2
c2为
i
i
i后面大于
a
[
j
]
a[j]
a[j]的数的个数。
k
2
k_2
k2满足
k
2
>
i
k_2>i
k2>i且
a
[
k
2
]
>
a
[
j
]
a[k_2]>a[j]
a[k2]>a[j]
设新的函数
f
(
i
)
=
∑
d
p
(
i
,
k
1
)
f(i)=\sum dp(i,k_1)
f(i)=∑dp(i,k1)
k
1
>
j
k_1>j
k1>j这一点由枚举(逆序枚举)顺序保证。
我们只需要控制
a
k
1
>
a
j
a_{k_1}>a_j
ak1>aj。
以及
g
(
j
)
=
∑
d
p
(
k
2
,
j
)
g(j)=\sum dp(k_2,j)
g(j)=∑dp(k2,j),其他类似。
发现当前处理完的
d
p
(
i
,
j
)
dp(i,j)
dp(i,j)可以为此时的
f
f
f贡献。
//
// Created by Artist on 2021/11/4.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
#define endl '\n'
const int maxn = 5003;
const int mod = 998244353;
int qpow(int aa,int n) {
int ans=1;
while(n) {
if(n&1) ans=1ll*aa*ans%mod;
aa=1ll*aa*aa%mod;
n>>=1;
}
return ans;
}
int a[maxn];
int dp[maxn][maxn];
int f[maxn],g[maxn];
int c1[maxn],c2[maxn];
int big[maxn];
int inv[maxn];
signed main() {
int n;scanf("%d",&n);
for(int i=1;i<=n;++i) scanf("%d",&a[i]);
for(int i=1;i<=n;++i) inv[i]=qpow(i,mod-2);
for(int i=n;i;--i) {
for(int j=n;j;--j) {
if(a[i]>a[j]) {
if(c1[i]) dp[i][j] = (1+1ll*inv[c1[i]]*f[i]%mod)%mod;
c2[j]++;
g[j]=(1ll*g[j]+dp[i][j])%mod;
}
if(a[j]>a[i]) {
if(c2[j]) dp[i][j] = (1+1ll*inv[c2[j]]*g[j]%mod)%mod;
c1[i]++;
f[i]=(1ll*f[i]+dp[i][j])%mod;
}
}
}
int ans=0;
int n1 = qpow(n,mod-2);
for(int i=1;i<=n;++i) {
for(int j=1;j<=n;++j) if(a[j]>a[i]) big[i]++;
}
for(int i=1;i<=n;++i) big[i]=qpow(big[i],mod-2);
for(int i=1;i<=n;++i) {
// 不存在
if(big[i]==0) {
// 1/n的概率来到这个局面,轮数仅为1
ans=(ans+n1)%mod;
continue;
}
for(int j=1;j<=n;++j) {
if(a[j]>a[i]) {
// 1/n*1/big的概率来到这个局面,轮数为后续+2
ans=(ans+1ll*n1*big[i]%mod*(dp[i][j]+2ll)%mod)%mod;
}
}
}
printf("%d\n",ans);
}
连通性复习
点双连通分量
一条边属于一个点双。
一个点属于一个点双当且仅当它不是割点。
特殊:两个点也可能是一个点双。
以下代码用于求出所有点双。
如下图,上面那个图会求出两个点双。下面那个图会求出四个点双。
int tarjan(int u,int fa){
// 记录边数
low[u] = num[u] = ++dfn;
s.push(u); instack[u] = 1;
// 栈里放的是点
for(int i=0;i<G[u].size();++i){
int v = G[u][i];
if(v==fa) continue;
if(!low[v]) {
tarjan(v,u);
low[u] = min(low[u],low[v]);
if(low[v]>=num[u]) { // 找到割点,意即找到了BCC
bcc.clear(); // 先清空
while(1){
int x = s.top();s.pop(),instack[x] = 0;
bcc.push_back(x);
if(x==v) break;
}
bcc.push_back(u);
solve(); // 处理当前BCC
}
}else if(instack[v]) low[u] = min(low[u],num[v]); // 回退边
}
}
仅仅求割点:
inline void tarjan(int u,int fa) {
num[u]=++dfn;low[u]=dfn;
int sn=0;
for(auto o:G[u]) {
int v=o.fi;
if(!num[v]) {
tarjan(v,u);
low[u]=min(low[v],low[u]);
if(low[v]>=num[u]) {
sn++;
if(u!=rt||sn>1) iscut[u]=1;
}
} else if(v!=fa) low[u]=min(low[u],num[v]);
}
}
边双连通分量
一条边要么仅属于一个边双,要么是割边。
一个点要么属于一个边双(可能还连着割边),要么是连接两条割边的点。
求割边很简单:
inline void tarjan(int u,int fa) {
num[u]=++dfn;low[u]=dfn;
int sn=0;
for(auto o:G[u]) {
int v=o.fi;
if(!num[v]) {
tarjan(v,u);
low[u]=min(low[v],low[u]);
if(low[v]>num[u]) iscute[o.se]=1;
} else if(v!=fa) low[u]=min(low[u],num[v]);
}
}
求边双:
做法1:去掉割边,对每个边双跑dfs。
找dcc中的边只需要对每个边判断是否两个点属于同一边双即可。
注意到这样做,可能一个点构成一个边双。判断一下该边双是否有边即可。
void dfs(int u) {
bel[u]=dcc;sz[dcc]++;
for(auto o:G[u]) {
int v=o.fi;
if(iscute[o.se]||bel[v]) continue;
dfs(v);
}
}
main函数中:
for(int i=1;i<=n;++i) {
if(!bel[i]) {
++dcc,sz[dcc]=sze[dcc]=0;
dfs(i);
}
}
for(int i=1;i<=m;++i) {
if(bel[ed[i].fi]==bel[ed[i].se]) ++sze[bel[ed[i].fi]];
}
for(int i=1;i<=dcc;++i) {
q=max(q,sze[i]); // 最多边的边双的边数
if(sz[i]>1) ++p; // 边双数
}
做法2:tarjan
可以用来求每个点属于哪个边双。
此时连接桥,不属于任一个边双的点被看作自己属于一个边双。
因此sz>1的就是一个边双。
求每条边属于哪个边双:
看这条边两个点是否同属于一个边双。不然就是割边。
stack<int> st;
inline void tarjan2(int u,int fa) {
num[u]=++dfn;low[u]=dfn;
st.push(u);
for(auto o:G[u]) {
int v=o.fi;
if(!num[v]) {
tarjan2(v,u);
low[u]=min(low[v],low[u]);
} else if(v!=fa) low[u]=min(low[u],num[v]);
}
if(num[u]==low[u]) {
dcc++;
int x;
do{
x=st.top();st.pop();
bel[x]=dcc;
sz[dcc]++;
}while(x!=u);
}
}
强连通分量
点双连通分量缩点
强连通分量缩点
树上问题复习
点分治&树上启发式合并对比
有一些问题用两种做法都可以做。
以下是一些优缺点:
点分治(复杂度依赖于换根,保证层数为log):
- 可以利用排序+双指针对数据进行处理,但是有时候需要容斥
- 也可以用离散化树状数组/动态开点线段树,这样就不用容斥了
- 因为会换根,所以做不了子树问题,主要用于树上路径问题
dsu on tree(复杂度依赖于树剖):
- 要考虑怎么快速集成重儿子信息
- 要考虑怎么合并信息
- 可以处理子树问题,路径问题
- 本质是树剖的运用
Cut the Tree(长链剖分)
每个轻链节点维护其子链最长LIS,总长
O
(
n
)
O(n)
O(n)。长链继承。
考虑如何求删点后最小LIS。从最长LIS链两端为根出发,记录某点子树最长链。然后把两次dfs记录的某点子树最长链取个max就包含了该点出发的子树的所有可能最长链、也就是避开了该点的最长链,也就是删除了该点后的最长链。
//
// Created by Artist on 2021/11/6.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
#define endl '\n'
#define mid (l+r>>1)
void dbg() { std::cout << " #\n"; }
template<typename T, typename...Args>
void dbg(T a, Args...args) {
std::cout << a << ' ';
dbg(args...);
}
void io() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); }
#define typeinput int
inline char nc() {
static char buf[1000000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1000000, stdin), p1 == p2) ? EOF : *p1++;
}
inline void read(typeinput &sum) {
char ch = nc();
sum = 0;
while (!(ch >= '0' && ch <= '9')) ch = nc();
while (ch >= '0' && ch <= '9') sum = (sum << 3) + (sum << 1) + (ch - 48), ch = nc();
}
int n;
const int maxn = 1e5+5;
vector<int> G[maxn];
int a[maxn];
int fa[maxn],ans[maxn],son[maxn],cnt,Ans,dep[maxn];
// LIS的长度以及两端
struct Data{
int ans,u,v;
} f[maxn];
struct vec{
int x,id; // 末尾节点的id
};
vector<vec> vt[2][maxn]; // 里面的vec的id都相等,都是这个vt末尾节点的id
// 上升和下降的最长序列栈
Data max(Data x,Data y) {
return x.ans>y.ans?x:y;
}
vec max(vec x,vec y) {
return x.x>y.x?x:y;
}
void ins(vector <vec> &Vt,int x,int id) {
int l=0,r=Vt.size()-1;
while(l<=r) {
if(Vt[mid].x>x) l=mid+1;
else r=mid-1;
}
// 如果都比当前插入的大
if(l==Vt.size()) {
// 如果大小不为0,推到后面
if(l) Vt.pb((vec){x,Vt[l-1].id});
// 如果是空,直接插入即可
else Vt.pb((vec){x,id});
}
else {
// 替换成更小的值
Vt[l].x=x;
if(l) Vt[l].id=Vt[l-1].id;
else Vt[l].id=id;
}
}
// 合并
void merge(vector<vec> &x,vector<vec> &y) {
if(x.size()<y.size()) swap(x,y);
for(int i=0;i<y.size();++i) x[i]=max(x[i],y[i]);
}
// 更新答案用
void upData(int x,int y) {
for(int i=0;i<vt[0][y].size();++i) {
int l=0,r=vt[1][x].size()-1;
while(l<=r) {
-vt[1][x][mid].x<vt[0][y][i].x?l=mid+1:r=mid-1;
}
--l;
if(l>=0) f[x]=max(f[x],(Data){l+i+2,vt[0][y][i].id,vt[1][x][l].id});
}
for(int i=0;i<vt[1][y].size();++i) {
int l=0,r=vt[0][x].size()-1;
while(l<=r) {
-vt[1][y][i].x<vt[0][x][mid].x?l=mid+1:r=mid-1;
}
--l;
if(l>=0) f[x]=max(f[x],(Data){l+i+2,vt[0][x][l].id,vt[1][y][i].id});
}
}
void dfs(int u,int pa) {
fa[u]=pa,dep[u]=1,f[u]=(Data){1,u,u},son[u]=0;
for(auto v:G[u]) {
if(v-pa) {
dfs(v,u);
dep[u]=max(dep[u],dep[v]+1);
if(dep[v]>dep[son[u]]) son[u]=v;
f[u]=max(f[u],f[v]); // 子树中的LIS
}
}
// 叶子
if(!son[u]) {
// 清空vector
vt[0][u].clear();vt[1][u].clear();
// 把自己算进去
ins(vt[0][u],a[u],u);// 自叶子到根的最长下降子序列
ins(vt[1][u],-a[u],u); // 自叶子到根的LIS
} else {
// 大概是一种继承并清空son[u]的方式()
swap(vt[0][u],vt[0][son[u]]);
swap(vt[1][u],vt[1][son[u]]);
// 将当前点插入进去
ins(vt[0][u],a[u],u);
ins(vt[1][u],-a[u],u);
for(auto v:G[u]) {
if(v-fa[u]&&v-son[u]) {
// 更新答案
upData(u,v);
// 把当前点(lca)插入到v里面,便于等下合并
ins(vt[0][v],a[u],u);
ins(vt[1][v],-a[u],u);
// 合并轻链到根链上
merge(vt[0][u],vt[0][v]);
merge(vt[1][u],vt[1][v]);
}
}
// 更新答案
for(int i=0;i<2;++i) {
int sz=vt[i][u].size();
f[u]=max(f[u],(Data){sz,u,vt[i][u][sz-1].id});
}
}
}
void solve(int u) {
dfs(u,0);
for(int x=1;x<=n;++x) {
for(auto v:G[x]) {
if(v-fa[x]) ans[x] = max(ans[x],f[v].ans);
}
}
}
signed main() {
io();
cin>>n;
for(int i=1;i<n;++i) {
int u,v;cin>>u>>v;
G[u].pb(v);
G[v].pb(u);
}
for(int i=1;i<=n;++i) {
cin>>a[i];
}
dfs(1,0);
int U=f[1].u,V=f[1].v; // 最长的那条LIS的起始点和终结点
solve(U),solve(V),Ans=n;
for(int x=U;x;x=fa[x]) Ans=min(Ans,ans[x]);
cout<<Ans<<endl;
}
Kuriyama Mirai and Exclusive Or
倍增+差分+lowbit(lowbit以下加等于异或)
#include<bits/stdc++.h>
using namespace std;
const int maxn = 6e5+5;
int di[maxn],bs[maxn][25];
int n,q;
int main() {
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>q;
for(int i=1,x;i<=n;++i) {
cin>>x;
di[i]^=x;
di[i+1]^=x;
}
while(q--) {
int opt,l,r,x;cin>>opt>>l>>r>>x;
if(!opt) {
di[l]^=x;
di[r+1]^=x;
} else {
// lowbit
for(int i=0;i<=20;++i) {
if(x&(1<<i)&&l+(1<<i)-1<=r) {
di[l]^=x;
di[l+(1<<i)]^=x;
bs[l][i]^=1;
x+=1<<i;
l+=1<<i;
}
}
// 倍增处理余数
for(int i=20;~i;--i) {
if(l+(1<<i)-1<=r) {
di[l]^=x;
di[l+(1<<i)]^=x;
bs[l][i]^=1;
x+=1<<i;
l+=1<<i;
}
}
}
}
// push down
for(int i=20;i;--i) {
for(int l=1;l<=n;++l) {
if(!bs[l][i]) continue;
bs[l][i-1]^=1; // 0往左区间推跟没推一样,所以不用推di
if(l+(1<<i-1)<=n) {
bs[l+(1<<i-1)][i-1]^=1;
di[l+(1<<i-1)]^=1<<i-1;
if(l+(1<<i)<=n) {
di[l+(1<<i)]^=1<<i-1;
}
}
}
}
for(int i=1;i<=n;++i) {
cout<<(di[i]^=di[i-1])<<' ';
}
}
杜教筛
g ( 1 ) S ( n ) = ∑ i = 1 n ( f ∗ g ) ( i ) − ∑ i = 2 n g ( i ) S ( ⌊ n i ⌋ ) g(1)S(n)=\sum_{i=1}^n(f*g)(i)-\sum_{i=2}^ng(i)S(\lfloor\frac{n}{i}\rfloor) g(1)S(n)=∑i=1n(f∗g)(i)−∑i=2ng(i)S(⌊in⌋)
//
// Created by Artist on 2021/11/9.
//
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define mkp make_pair
#define fi first
#define se second
#define pb push_back
#define all(x) x.begin(),x.end()
#define DB1(args...) do { cout << #args << " : "; dbg(args); } while (0)
#define endl '\n'
void io() { ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); }
const int maxn = 1e6+4;
int vis[maxn],pri[maxn],cnt;
ll mu[maxn],pre_mu[maxn];
ll pre_phi[maxn],phi[maxn];
map<ll,ll> mpmu;
map<ll,ll> mpphi;
// 首先预处理出一定范围内的莫比乌斯函数的前缀和
void init() {
mu[1]=1;phi[1]=1;
for(int i=2;i<maxn;++i) {
if(!vis[i]) pri[++cnt]=i,mu[i]=-1,phi[i]=i-1;
for(int j=1;j<=cnt&&1ll*i*pri[j]<maxn;++j) {
vis[i*pri[j]]=1;
if(i%pri[j]==0) {
mu[i*pri[j]] = 0;
phi[i*pri[j]] = phi[i]*pri[j];
break;
} else {
mu[i*pri[j]] = -mu[i];
phi[i*pri[j]] = phi[i]*(pri[j]-1);
}
}
}
for(int i=1;i<maxn;++i) pre_mu[i]=pre_mu[i-1]+mu[i];
for(int i=1;i<maxn;++i) pre_phi[i]=pre_phi[i-1]+phi[i];
}
// 求莫比乌斯前缀和
ll getpremu(ll x) {
if(x<maxn) return pre_mu[x];
if(mpmu[x]) return mpmu[x];
ll ans=1;
for(ll i=2,j;i<=x;i=j+1) {
j=min(x,x/(x/i));
ans -= getpremu(x/i)*(j-i+1);
}
return mpmu[x]=ans;
}
// 求欧拉函数前缀和
ll getprephi(ll x) {
if(x<maxn) return pre_phi[x];
if(mpphi[x]) return mpphi[x];
ll ans = 0;
ll lst = 0;
for(ll i=1,j;i<=x;i=j+1) {
j=min(x,x/(x/i));
ll pre = getpremu(j);
ans += 1ll*(pre-lst)*(x/i)*(x/i);
lst=pre;
}
--ans;
ans/=2;
return mpphi[x]=ans+1;
}
signed main() {
io();
int t;cin>>t;
init();
while(t--) {
ll n;cin>>n;
cout<<getprephi(n)<<" "<<getpremu(n)<<endl;
}
}