【HNOI 2018】毒瘤

Problem

Description

从前有一名毒瘤。

毒瘤最近发现了量产毒瘤题的奥秘。考虑如下类型的数据结构题:给出一个数组,要求支持若干种奇奇怪怪的修改操作(例如给一个区间内的数同时加上 c c c,或者将一个区间内的数同时开平方根),并且支持询问区间的和。毒瘤考虑了 n n n 个这样的修改操作,并将它们编号为 1 … n 1 \ldots n 1n。当毒瘤要出数据结构题的时候,他就将这些修改操作中选若干个出来,然后出成一道题。

当然了,这样出的题有可能不可做。通过精妙的数学推理,毒瘤揭露了这些修改操作之间的关系:有 m m m 对「互相排斥」的修改操作,第 i i i 对是第 u i u_i ui 个操作和第 v i v_i vi 个操作。当一道题中同时含有 u i u_i ui v i v_i vi 这两个操作时,这道题就会变得不可做。另一方面,当一道题中不包含任何「互相排斥」的操作时,这个题就是可做的。此外,毒瘤还发现了一个规律: m − n m − n mn 是一个很小的数字(参见「数据范围」中的说明),且任意两个修改操作都是连通的。两个修改操作 a , b a, b a,b 是连通的,当且仅当存在若干操作 t 0 , t 1 , . . . , t l t_0, t_1, ... , t_l t0,t1,...,tl,使得 t 0 = a , t l = b t_0 = a,t_l = b t0=a,tl=b,且对任意 1 ≤ i ≤ l 1 \le i \le l 1il t i − 1 t_{i−1} ti1 t i t_i ti 都是「互相排斥」的修改操作。

一对「互相排斥」的修改操作称为互斥对。现在毒瘤想知道,给定值 n n n m m m 个互斥对,他一共能出出多少道可做的不同的数据结构题。两个数据结构题是不同的,当且仅当其中某个操作出现在了其中一个题中,但是没有出现在另一个题中。

Input Format

第一行为正整数 n , m n, m n,m

接下来 m m m 行,每行两个正整数 u , v u, v u,v,代表一对「互相排斥」的修改操作。

Output Format

输出一行一个整数,表示毒瘤可以出的可做的不同的数据结构题的个数。这个数可能很大,所以只输出模 998244353 998244353 998244353 后的值。

Sample

Input 1

3 2
1 2
2 3

Output 1

5

Input 2

6 8
1 2
1 3
1 4
2 4
3 5
4 5
4 6
1 6

Output 2

16

Input 3

12 18
12 6
3 11
8 6
2 9
10 4
1 8
6 2
11 5
10 6
12 2
9 3
7 6
2 7
3 2
7 3
5 6
2 11
12 1

Output 3

248

Range

测试点 #1~45~67~8910~1112~1415~1617~20
n ≤ n \le n 20 20 20 1 0 5 10^5 105 1 0 5 10^5 105 3000 3000 3000 1 0 5 10^5 105 3000 3000 3000 1 0 5 10^5 105 1 0 5 10^5 105
m ≤ m \le m n + 10 n + 10 n+10 n − 1 n - 1 n1 n n n n + 1 n + 1 n+1 n + 1 n + 1 n+1 n + 10 n + 10 n+10 n + 7 n + 7 n+7 n + 10 n + 10 n+10

Algorithm

D P DP DP,虚树

Mentality

这题真的是,题如其名,我 t m tm tm 码了 3.4 k . . . . . . 3.4k...... 3.4k......

我们先来考虑暴力 80 p t s 80pts 80pts (实际上有 85 p t s 85pts 85pts 呢) 。

D P DP DP 式很显然:

f [ i ] [ 0 ] = ∏ ( f [ s o n ] [ 0 ] + f [ s o n ] [ 1 ] ) f [ i ] [ 1 ] = ∏ f [ s o n ] [ 0 ] f[i][0]=\prod (f[son][0]+f[son][1])\\ f[i][1]=\prod f[son][0] f[i][0]=(f[son][0]+f[son][1])f[i][1]=f[son][0]

当然, A n s = f [ 1 ] [ 0 ] + f [ 1 ] [ 1 ] Ans=f[1][0]+f[1][1] Ans=f[1][0]+f[1][1]

不过我们还多出来一些非树边,怎么办?其实很简单,由于非树边两端点会互相影响,那我们只需要枚举每个与非树边相连的点是选还是不选,然后将 D P DP DP 数组的相关值改为 0 0 0 ,再做一遍 D P DP DP 即可。

由于每个点的情况与非树边相关,我们只需要枚举每条非树边的左端点 u u u (输入中先输入的那个端点) 是选还是不选,如果选,那么将 f [ u ] [ 0 ] f[u][0] f[u][0] 赋值为 0 0 0 ,因为我们已经钦定此点会被选择;同理 f [ v ] [ 1 ] f[v][1] f[v][1] 也要赋值为 0 0 0 。而如果 u u u 不选,那就不需要再管 v v v 了,因为 v v v 不受影响。

枚举部分代码如下:

for(int S=0;S<(1<<top);S++)//top 是非树边个数
{
    for(int i=1;i<=n;i++)f[i][0]=f[i][1]=1;
    for(int i=1;i<=top;i++)//相关值赋为 0
        if(S&(1<<(i-1)))
            f[U[i]][0]=0,f[V[i]][1]=0;
        else
            f[U[i]][1]=0;
    DP();//DP
    ans=(ans+(f[1][0]+f[1][1])%mod)%mod;//加入答案
}

那对于 100 100 100 分的部分分怎么做呢?

其实做过暴力的话,也差不多能想到该优化哪个方面了:每次枚举之后的 D P DP DP

因为每次只改变了至多 22 22 22 个点的状态,所以我们应该想办法避免重复计算那些无关的点的 D P DP DP 值。

那显然是 动态dp 建虚树啊 。

那么如何优化点与点的 D P DP DP 计算呢?我们可以发现一件事情:由于 D P DP DP 过程中,我们的运算都是乘法运算,所以在虚树上若有边 u − &gt; v u-&gt;v u>v ,则我们必定可以得到

f [ u ] [ 0 ] = a × f [ v ] [ 0 ] + b × f [ v ] [ 1 ] f [ u ] [ 1 ] = c × f [ v ] [ 0 ] + d × f [ v ] [ 1 ] f[u][0]=a×f[v][0]+b×f[v][1]\\ f[u][1]=c×f[v][0]+d×f[v][1] f[u][0]=a×f[v][0]+b×f[v][1]f[u][1]=c×f[v][0]+d×f[v][1]

其中 a , b , c , d a,b,c,d a,b,c,d 均为可以计算的未知数,不妨将其称之为 v v v 在虚树上转移的系数。

我们分别设为 k 0 [ v ] [ 0 ] , k 0 [ v ] [ 1 ] k0[v][0],k0[v][1] k0[v][0],k0[v][1] 代表 f [ v ] [ 0 ] f[v][0] f[v][0] 分别为 f [ u ] [ 0 ] , f [ u ] [ 1 ] f[u][0],f[u][1] f[u][0],f[u][1] 有多少系数的贡献; k 1 [ v ] [ 0 ] , k 1 [ v ] [ 1 ] k1[v][0],k1[v][1] k1[v][0],k1[v][1] 同理。

这部分的式子及代码如下:

for(int i=x;fa[i][0]!=y;i=fa[i][0])
{
    int Fa=fa[i][0];
    work(Fa,i);//计算每层节点不含虚树点的子树的 dp 值
    int t0=k0[x][0],t1=k1[x][0];
    k0[x][0]=1ll*f[Fa][0]*(t0+k0[x][1])%mod;
    k1[x][0]=1ll*f[Fa][0]*(t1+k1[x][1])%mod;
    k0[x][1]=1ll*f[Fa][1]*t0%mod;
    k1[x][1]=1ll*f[Fa][1]*t1%mod;
}

那么思路就很简单了,求出虚树上每个点到父结点的实际子节点的转移系数,然后 D P DP DP 的时候利用转移系数 D P DP DP 就好。由于虚树的性质,每个点的一棵虚树子树内只会有一个直接相连的点,否则子树内的两个点的 l c a lca lca 也会是关键点 . . . . . . ...... ...... 所以不用担心转移问题。

求系数详见代码。

虽然题解超级不详细 (没办法题目毒得我不知何去何从) 。

Code

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int mod=998244353;
int n,m,ans,fa[100001][18],head[100001],nx[200001],to[200001];
int now,top,cnt,sum,sumk,num,key[23],U[23],V[23],tree[100001],stack[100001],dfn[100001],deep[100001];
int hd2[100001],nx2[100001],to2[100001],g[100001][2],f[100001][2],k0[100001][2],k1[100001][2];
bool vis[100001],book[100001];
int find(int x){return fa[x][0]==x?x:fa[x][0]=find(fa[x][0]);}
bool cmp(int a,int b){return dfn[a]<dfn[b];}
void addroad(int u,int v,int d)
{
    to[d]=v,nx[d]=head[u];
    head[u]=d;
}
void build(int x,int pa)
{
    deep[x]=deep[pa]+1,dfn[x]=++cnt,fa[x][0]=pa;
    for(int i=1;i<=17;i++)fa[x][i]=fa[fa[x][i-1]][i-1];
    for(int i=head[x];i;i=nx[i])
        if(to[i]!=pa)
            build(to[i],x);
}
int getlca(int a,int b)
{
    if(deep[a]<deep[b])swap(a,b);
    for(int i=17;i>=0;i--)
        if(deep[fa[a][i]]>=deep[b])
            a=fa[a][i];
    for(int i=17;i>=0;i--)
        if(fa[a][i]!=fa[b][i])
            a=fa[a][i],b=fa[b][i];
    return a==b?a:fa[a][0];
}
void link(int a,int b)
{
    if(!book[a])tree[++num]=a;
    if(!book[b])tree[++num]=b;
    book[a]=book[b]=true,cnt++;
    to2[cnt]=b,nx2[cnt]=hd2[a];
    hd2[a]=cnt;
}
void Insert(int x)
{
    int lca=getlca(stack[top],x);
    while(top>1&&dfn[stack[top-1]]>=dfn[lca])
        link(stack[--top],stack[top]);
    if(lca!=stack[top])
        link(lca,stack[top]),stack[top]=lca;
    stack[++top]=x;
}
void work(int x,int y)//正常计算 dp 值,但是不会计算标记点,标记点一般为含虚树点的子树
{
    f[x][0]=f[x][1]=vis[x]=1;
    for(int i=head[x];i;i=nx[i])
        if(to[i]!=fa[x][0]&&to[i]!=y&&!vis[to[i]])
        {
            work(to[i],y);
            f[x][0]=1ll*f[x][0]*(f[to[i]][0]+f[to[i]][1])%mod;
            f[x][1]=1ll*f[x][1]*f[to[i]][0]%mod;
        }
}
void getk(int x,int y)
{
    vis[x]=k0[x][0]=k1[x][1]=1;
    for(int i=x;fa[i][0]!=y;i=fa[i][0])
    {
        int Fa=fa[i][0];
        work(Fa,i);//层层计算每层的答案,每个节点不含虚树点的子树的 dp 值也会产生贡献
        int t0=k0[x][0],t1=k1[x][0];
        k0[x][0]=1ll*f[Fa][0]*(t0+k0[x][1])%mod;
        k1[x][0]=1ll*f[Fa][0]*(t1+k1[x][1])%mod;
        k0[x][1]=1ll*f[Fa][1]*t0%mod;
        k1[x][1]=1ll*f[Fa][1]*t1%mod;
    }
}
void Count(int x)
{
    for(int i=hd2[x];i;i=nx2[i])
        Count(to2[i]),getk(to2[i],x);//计算系数
    f[x][0]=f[x][1]=1;
    for(int i=head[x];i;i=nx[i])
        if(!vis[to[i]]&&to[i]!=fa[x][0])
        {
            work(to[i],0);
            f[x][0]=1ll*f[x][0]*(f[to[i]][0]+f[to[i]][1])%mod;
            f[x][1]=1ll*f[x][1]*f[to[i]][0]%mod;
        }//计算非虚树部分的 dp 值
}
void DP(int x)
{
    for(int i=hd2[x];i;i=nx2[i])
    {
        int p=to2[i];
        DP(p);
        int f0=(1ll*k0[p][0]*g[p][0]+1ll*k1[p][0]*g[p][1]%mod)%mod;
        int f1=(1ll*k0[p][1]*g[p][0]+1ll*k1[p][1]*g[p][1]%mod)%mod;
        g[x][0]=1ll*g[x][0]*(f0+f1)%mod,g[x][1]=1ll*g[x][1]*f0%mod;//直接乘系数计算就好了
    }
}
int main()
{
    cin>>n>>m;
    int u,v;
    for(int i=1;i<=n;i++)fa[i][0]=i;
    vis[1]=true;
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&u,&v);
        if(find(u)==find(v))
        {
            sum++;
            U[sum]=u;
            if(!vis[u])key[++sumk]=u;
            V[sum]=v;
            if(!vis[v])key[++sumk]=v;
            vis[u]=vis[v]=true;
        }//利用并查集判断那些边是非树边
        else
        {
            addroad(u,v,++cnt),addroad(v,u,++cnt);
            fa[find(v)][0]=fa[u][0];
        }
    }
    build(1,0);
    sort(key+1,key+sumk+1,cmp);
    cnt=0,stack[top=1]=1;
    for(int i=1;i<=n;i++)vis[i]=0;
    for(int i=1;i<=sumk;i++)
        Insert(key[i]);//构建虚树
    while(top>0)
        link(stack[--top],stack[top]);
    Count(1);//计算系数,并预处理每个节点不计算含有虚树的子树的 dp 值
    for(int S=0;S<(1<<sum);S++)
    {
        for(int i=1;i<=num;i++)
            g[tree[i]][0]=f[tree[i]][0],g[tree[i]][1]=f[tree[i]][1];//赋初值
        for(int i=1;i<=sum;i++)//枚举状态的相关赋值
            if(S&(1<<(i-1)))
                g[U[i]][0]=0,g[V[i]][1]=0;
            else
                g[U[i]][1]=0;
        DP(1);//DP
        ans=(ans+(g[1][0]+g[1][1])%mod)%mod;//计算答案
    }
    cout<<ans;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据引用\[1\]和引用\[2\]的描述,题目中的影魔拥有n个灵魂,每个灵魂有一个战斗力ki。对于任意一对灵魂对i,j (i<j),如果不存在ks (i<s<j)大于ki或者kj,则会为影魔提供p1的攻击力。另一种情况是,如果存在一个位置k,满足ki<c<kj或者kj<c<ki,则会为影魔提供p2的攻击力。其他情况下的灵魂对不会为影魔提供攻击力。 根据引用\[3\]的描述,我们可以从左到右进行枚举。对于情况1,当扫到r\[i\]时,更新l\[i\]的贡献。对于情况2.1,当扫到l\[i\]时,更新区间\[i+1,r\[i\]-1\]的贡献。对于情况2.2,当扫到r\[i\]时,更新区间\[l\[i\]+1,i-1\]的贡献。 因此,对于给定的区间\[l,r\],我们可以根据上述方法计算出区间内所有下标二元组i,j (l<=i<j<=r)的贡献之和。 #### 引用[.reference_title] - *1* *3* [P3722 [AH2017/HNOI2017]影魔(树状数组)](https://blog.csdn.net/li_wen_zhuo/article/details/115446022)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [洛谷3722 AH2017/HNOI2017 影魔 线段树 单调栈](https://blog.csdn.net/forever_shi/article/details/119649910)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值