十一月刷题记录2

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+c11dp(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+c21dp(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):

  1. 可以利用排序+双指针对数据进行处理,但是有时候需要容斥
  2. 也可以用离散化树状数组/动态开点线段树,这样就不用容斥了
  3. 因为会换根,所以做不了子树问题,主要用于树上路径问题

dsu on tree(复杂度依赖于树剖):

  1. 要考虑怎么快速集成重儿子信息
  2. 要考虑怎么合并信息
  3. 可以处理子树问题,路径问题
  4. 本质是树剖的运用

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(fg)(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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值