HGOI 20190821 慈溪一中互测

Problem A 

给出一个$n$个点$m$条边的仙人掌图(每条边最多处于一个简单环中)。

使用$c$种颜色对这张图中的顶点染色,使得每一条无向边连接的两个点颜色不同。

求染色的方案数,$mod \ 998244353$的值。

对于$100\%$的数据满足,$ 1 \leq n,m \leq 10^6$

Solution : 

  对于一棵树的答案非常简单就是$c \times (c-1) ^ {n-1}$

  对于一个大环,我们不妨计算这个环上的方案数。

  设$f(n)$表示含有$n$个点的环上使用$c$种颜色染色的方案数,

  非常显然,$f(1) = c$ , $f(2) = c(c-1)$.

  若$c \geq 3$ 那么考虑对于所有节点不能和前一个刚被染色的节点重复染色,方案数就是$c \times (c-1)^{n-1}$

  然后考虑,最后一个点和第一个点的颜色不能重复,若重复,我们可以将其看做一个点,那么就变成了一个子问题。

  最终,$f(n) = c \times (c-1)^{n-1} - f(n-1)$

  对于仙人掌图我们考虑v-DCC缩点(即找点双联通分量)  

  对于一个点,可能在多个简单环里。

  那么对于这个点,只要被一个环计算过,那么在计算剩余环的时候应当认为这个点的颜色既定。

  所以,若多于一个点在$k$个简单环里,我们只需要将总方案数直接除以$c^{k-1}$即可,这是由于该点在其他$k-1$个环中被认为可以被染$c$种颜色,然而事实上,这个点的颜色在第一个环包含它的时候已经被计算。

  所以,本题只需要进行一遍tarjan的v-DCC缩点即可。

  复杂度$O(n)$

# include <bits/stdc++.h>
# define int long long
using namespace std;
const int N=1e6+10,mo=998244353;
int dfn[N],low[N],n,m,tot,head[N],cnt,blo[N],f[N],c;
vector<int>dcc[N];
stack<int>s;
struct rec{
    int pre,to;
}a[N<<2];
void adde(int u,int v) {
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
}
int Pow(int x,int n) {
    int ans = 1;
    while (n) {
        if (n&1) ans=ans*x%mo;
        x=x*x%mo;
        n>>=1;
    }
    return ans%mo;
}
void tarjan(int u) {
    dfn[u] = low[u] = ++dfn[0]; s.push(u);
    for (int i=head[u];i;i=a[i].pre){
        int v=a[i].to; 
        if (!dfn[v]) {
            tarjan(v); low[u]=min(low[u],low[v]);
            if (low[v]>=dfn[u]) {
                int z; ++cnt;
                do {
                    z = s.top(); s.pop();
                    dcc[cnt].push_back(z);
                    blo[z]++;
                }while (v!=z);
                dcc[cnt].push_back(u);
                blo[u]++;
            }
        } else low[u] = min(low[u],dfn[v]);
    }
}
 main()
{
    scanf("%lld%lld%lld",&n,&m,&c);
    for (int i=1;i<=m;i++) {
        int u,v; scanf("%lld%lld",&u,&v);
        adde(u,v); adde(v,u);
    }
    f[1] = c; f[2] = c*(c-1) % mo;
    for (int i=3;i<=n;i++) 
        f[i]=((c*Pow(c-1,i-1)%mo-f[i-1])%mo+mo)%mo;
    tarjan(1);
    int ans = 1,ret = 0;
    for (int i=1;i<=n;i++) ret+=blo[i]-1;
    for (int i=1;i<=cnt;i++)
        ans = ans*f[dcc[i].size()]%mo;
    int ni = Pow(c,mo-2);   
    ans = ans * Pow(ni,ret) % mo;
    printf("%lld\n",ans);
    return 0;
}
A.cpp

Problem B

有一个集合S,初始时为空.给定$n,k$

每一次操作,你可以选择1到n中的任意一个不在S中的元素i,并将其加入S,此时如果i-2也在S中,则将i-2从S中删除,

如果i+k也在S中,则将i+k从S中删除。该操作可以进行任意多次。郝仁想知道总共有多少种不同的S,对998244353取模

对于$100\%$的数据$n \leq 300$

Solution : 

  首先将集合转化为一个有向图,对于每一个$i$若存在$i-2$和$i+k$,则从$i$连出一条边。

  这样本题就转化为一个图上的计数题。

  • 若$k$为偶数。

    显然序号为奇数和序号为偶数互不干扰,我们可以将奇数序号和偶数序号的分别进行考虑。

    问题便转化为有$n$个数里面选择若干个数,最多可以连续选择$m$个数字,方案数。

    最后将偶数的方案总数乘以奇数的方案总数即可。

    于是我们定义$f_{i,j}$表示前$i$个数字,当前已经连续选择$j$个数字,方案总数。

    转移可以采用刷表法:$f_{i+1,0}+=f_{i,j} , f_{i+1,j+1}+=f_{i,j}$

    复杂度是$O(nm)$的。

  • 若$k$为奇数。

    奇数序号和偶数序号相关,我们可以将$1  - n$的序号从小到大从上到下,奇数在左边,偶数在右边。

    $i$和$i+k$对齐,这样构成的矩阵,高是$\left \lfloor \frac{n+1}{2} \right \rfloor+ \left \lfloor \frac{k}{2} \right \rfloor$

    其中,右侧偶数非空的区间是$[1,\left \lfloor \frac{n}{2}  \right \rfloor]$

    左侧奇数的非空区间是$[\left \lfloor \frac{k}{2} \right \rfloor , \left \lfloor \frac{n+1}{2} \right \rfloor+ \left \lfloor \frac{k}{2} \right \rfloor]$

    上述结论是基本性质,归纳可证。

    考虑一个dp,设$f[i][j][k]$表示当前已经考虑到第$i$层了(从上到下,层数从$1$ 到 $\left \lfloor \frac{n+1}{2} \right \rfloor+ \left \lfloor \frac{k}{2} \right \rfloor$), 在右侧偶数已经连续选择了$j$个元素(为0则为空),在左侧奇数最多连续选择了$k$个元素,且必须包含一个拐弯(我们认为从偶数列跳到奇数列为一个"拐弯")

    我们考虑第$i$层,左选/不选,右选/不选的情况,就可以得到四个DP方程。

    即

      • (左右都不选择,无限制)  : f[i+1][0][0]=f[i][j][k]
      • (左选右不选择,左边的元素存在,即$i \in [\left \lfloor \frac{k}{2} \right \rfloor , \left \lfloor \frac{n+1}{2} \right \rfloor+ \left \lfloor \frac{k}{2} \right \rfloor]$)

       $ f[i+1][0][k?k+1:0]+=f[i][j][k]$ , 若原来没有或者没有拐弯,那么现在也不会有。

      • (左不选右选,右边的元素存在,即$i \in [1,\left \lfloor \frac{n}{2}  \right \rfloor )$)

       $f[i+1][j+1][0]+=f[i][j][w]$ 

      • (既选择左,又选择右侧元素,左右元素同时存在,上述两个集合的交集)

$f[i+1][j+1][max(w+1,j+2)]+=f[i][j][w]$ , 左侧的状态有两条路来走,我们取较大的一条。

上述DP细节较多,可以采用刷表实现,复杂度会是$O(n^3)$. 

# include <bits/stdc++.h>
# define int long long
using namespace std;
const int N=305,mo=998244353;
int n,k;
namespace Subtask1 {
    int f[N][N][N];
    void main() {
        f[0][0][0] = 1;
        for (int i=0;i<(n+1)/2+k/2;i++)
         for (int j=0;j<=n;j++)
          for (int w = 0 ; w <= k+1 ; w++) {
            (f[i+1][0][0]+=f[i][j][w])%=mo;
            if (i>=(k/2)) (f[i+1][0][w?w+1:0]+=f[i][j][w])%=mo;
            if (i<n/2) (f[i+1][j+1][0]+=f[i][j][w])%=mo;
            if (i>=(k/2) && i<(n/2)) (f[i+1][j+1][max(w+1,j+2)]+=f[i][j][w])%=mo;
          }
        int ans = 0;
        for (int j=0;j<=n;j++)
         for (int w=0;w<=k+1;w++)
          (ans+=f[(n+1)/2+k/2][j][w])%=mo;
        cout<<ans<<'\n';
        exit(0);
    }
}
namespace Subtask2 {
    int f[N][N];
    int g(int n,int m) {
        memset(f,0,sizeof(f)); f[0][0] = 1;
        for (int i=0;i<=n;i++)
        for (int j=0;j<=m;j++) {
            f[i+1][0]=(f[i+1][0]+f[i][j])%mo;
            if (j+1<=m) f[i+1][j+1]=(f[i+1][j+1]+f[i][j])%mo;
        }
        int ret=0;
        for (int i=0;i<=m;i++) ret=(ret+f[n][i])%mo;
        return ret % mo;
    }
    void main() {
        int ans = 1ll * g((int)ceil((double)n/2.0),k/2) * g(n/2,k/2) % mo;
        cout<<ans<<'\n';
        exit(0);
    }
}
signed main() {
    cin>>n>>k;
    if (k&1) Subtask1::main();
    else Subtask2::main();
    return 0;
}
B.cpp

 Problem C      

  维护序列$a_i$支持$q$次查询操作,形如$l,r,d$表示在区间$[l,r]$中是$d$倍数的数的个数。

  对于$100\%$的数据满足$n,q,a_i \leq 10^5$

  Solution : 直接暴力分块,每一块中维护这个块内约数为$i , i \in [1,10^5]$的数的个数,方便$O(1)$查询。

       对于块外元素,直接暴力,对于块内元素,直接跑块即可。

       时空复杂度都是$O(n \sqrt{n})$  。

# pragma GCC optimize(3)
# include <bits/stdc++.h>
# define Rint register int
using namespace std;
const int N=1e5+1;
struct rec{
    int l,r,cnt[N];
}tr[318];
int n,m,a[N],blong[N],block,num;
inline int read()
{
    int X=0; char c=0;
    while(c<'0'||c>'9') c=getchar();
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return X;
}
void write(Rint x) {
    if (x>9) write(x/10);
    putchar('0'+x%10);
}
inline void writeln(Rint x) {
    write(x); putchar('\n');
}
int query(int l,int r,int d)
{
    int ret = 0;
    if (blong[l] == blong[r]) {
        for (Rint i=l;i<=r;i++)
         if (a[i]%d == 0) ret++;
        return ret; 
    }
    for (Rint i=l;i<=tr[blong[l]].r;i++) 
     if (a[i]%d == 0) ret++;
    for (Rint i=tr[blong[r]].l;i<=r;i++) 
     if (a[i]%d == 0) ret++;
    for (Rint i = blong[l]+1 ; i<=blong[r]-1; i++) 
     if (d<=n) ret+=tr[i].cnt[d];
    return ret;  
}
int main()
{
    n = read(); m =read();
    for (Rint i=1;i<=n;i++) a[i]=read();
    block=sqrt(n); 
    num=n/block; if (n%block) num++;
    for (Rint i=1;i<=num;i++) {
        tr[i].l=(i-1)*block+1,
        tr[i].r=i*block;
    }
    tr[num].r=n;
    for (Rint i=1;i<=n;i++) {
        blong[i]=(i-1)/block+1;
        for (Rint j=1;j<=sqrt(a[i]);j++) {
            if (a[i]%j!=0) continue; 
            tr[blong[i]].cnt[j]++; 
            if (j*j!=a[i]) tr[blong[i]].cnt[a[i]/j]++;
        } 
    }
    int l,r,d;
    while (m--) {
        l = read(),r =read(),d=read(); 
        query(l,r,d);
        writeln(query(l,r,d));
    }
    return 0;
}
C.cpp

 

posted @ 2019-08-21 20:23  ljc20020730  阅读(220)  评论(0编辑  收藏  举报