peiwenjun's blog 没有知识的荒原

NOIP 2022 提高组 题解

A.种花

题目描述

\(T\) 组数据,给定一张 \(n\times m\) 的网格图,部分位置有土坑。

下图分别展示了合法的 C-形和 F-形种花方案:

...... ......
.*--.. .*---.
.|.... .|....
.|.... .*--..
.*---. .|....
...... ......

其中所有非 . 的格子都不是土坑,注意 -| 的长度没有限制。

求有多少种 C-形和 F-形种花方案,对 \(998244353\) 取模。

数据范围

  • \(1\le T\le 5,1\le n,m\le 10^3\)

时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{512MB}\)

分析

枚举下面 * 的位置,前缀和统计上面 * 的方案数之和。

时间复杂度 \(\mathcal O(nm)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1005,mod=998244353;
int c,f,m,n,t,id;
bool b[maxn][maxn];
char ch[maxn];
int d[maxn][maxn],r[maxn][maxn];
int s[maxn];
int askc()
{
    if(!c) return 0;
    int res=0;
    for(int j=1;j<=m;j++)
        for(int i=1;i<=n;i++)
        {
            if(b[i][j]) s[i]=s[i-1]=0;
            else s[i]=s[i-1]+r[i][j];
            if(i>=2) res=(res+1ll*s[i-2]*r[i][j])%mod;
        }
    return res;
}
int askf()
{
    if(!f) return 0;
    int res=0;
    for(int j=1;j<=m;j++)
        for(int i=1;i<=n;i++)
        {
            if(b[i][j]) s[i]=s[i-1]=0;
            else s[i]=s[i-1]+r[i][j];
            if(i>=2) res=(res+1ll*s[i-2]*r[i][j]*d[i][j])%mod;
        }
    return res;
}
int main()
{
    scanf("%d%d",&t,&id);
    while(t--)
    {
        scanf("%d%d%d%d",&n,&m,&c,&f);
        for(int i=1;i<=n;i++)
        {
            scanf("%s",ch+1);
            for(int j=1;j<=m;j++) b[i][j]=ch[j]=='1';
        }
        for(int i=1;i<=n;i++)
            for(int j=m,cur=0;j>=1;j--)
            {
                if(b[i][j]) r[i][j]=cur=0;
                else r[i][j]=cur++;
            }
        for(int j=1;j<=m;j++)
            for(int i=n,cur=0;i>=1;i--)
            {
                if(b[i][j]) d[i][j]=cur=0;
                else d[i][j]=cur++;
            }
        printf("%d %d\n",askc(),askf());
    }
    return 0;
}

B.喵了个喵

题目描述

\(T\) 组数据,牌堆中有 \(m\) 张卡牌,每张牌属于 \(k\) 种颜色之一。

\(n\) 个栈,初始为空,目标是用这 \(n\) 个栈将所有卡牌全部消去。

有两种操作:

  • 选择一个栈,将牌堆顶的卡牌放入栈的顶部。如果操作后栈的最上方两张卡牌颜色相同,自动将这两张卡牌消去。
  • 选择两个不同的栈,如果栈底卡牌颜色相同,则将这两张卡牌消去,颜色不同则什么也不会做。

数据范围

  • \(1\le T\le 1005,1\le n\le 300,k\in\{2n-2,2n-1\},1\le\sum m\le 2\cdot 10^6\) ,保证每种颜色卡牌数量均为偶数。

时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{512MB}\)

分析

\(k=2n-2\) ,将第 \(n\) 个栈作为空栈,保证前 \(n-1\) 个栈的大小都不超过 \(2\) ,如果出现相同颜色的卡牌即可配对消去。

\(k=2n-1\) ,想法也很简单:

每种颜色至多留一张在栈中,并且保证出现相同颜色的卡牌可以直接消去。

如果栈中卡牌总数 \(\lt 2n-2\) ,随便找个栈放进去即可,注意别把空栈消耗了。

如果栈中卡牌总数 \(=2n-2\) (即 \(n-1\) 个大小为 \(2\) 的栈和一个空栈),并且出现了一种新的颜色。

我们根据牌堆中的下一张牌来定策略:

  • 如果下一张牌的同类牌在栈底,把牌堆顶放到这个栈中即可。
image

注意代码实现时不需要考虑这类情况,因为可以被合并到下一类中 \(cnt\) 为偶数的部分。


  • 如果下一张牌的同类牌在栈顶,又该怎么办呢?

策略一:找到下一次出现同类牌在栈底的位置,然后放到相应的栈中。

下图就是一个很好的例子:

image

但这并不足以解决本题,如果出现下面这种情况,策略一就失效了:

image

但是对于这种情况,如果把第一张牌放入空栈,好像又可以了!

策略二:将牌堆顶放入空栈。

image

但是如何决策应该使用哪个策略?

记后面第一次出现同类牌在栈底的位置为\(x\)

如果 \([i,x]\) 中这个栈的栈顶出现了奇数次(这意味着栈顶可以被消去),那么使用策略一,否则使用策略二。

时间复杂度 \(\mathcal O(\sum m)\)

#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=2e6+5;
int k,m,n,t,emp;///emp表示空栈
int a[maxn],bel[maxn];///bel[i]表示第i种颜色的牌在哪个堆中
queue<int> q;///q存储哪些栈当前元素个数<=1
vector<int> s[maxn];///s[i]表示第i个栈
vector<pii> res;
void push1(int col,int x)
{///操作一:在第x个栈中放入一张颜色为col的牌
    if(!s[x].empty()&&s[x].back()==col) s[x].pop_back();
    else s[x].push_back(col);
    res.push_back(mp(x,0));
}
void push2(int x,int y)
{///操作二:消去第x个栈和第y个栈的栈底
    assert(x!=y&&*s[x].begin()==*s[y].begin());
    s[x].erase(s[x].begin()),s[y].erase(s[y].begin());
    res.push_back(mp(x,y));
}
void work(int col)
{///常规方法加入一张颜色为col的牌
    if(bel[col])
    {
        int x=bel[col];
        assert(s[x].size()<=2);
        if(s[x].back()==col) push1(col,x);
        else assert(*s[x].begin()==col),push1(col,emp),push2(x,emp);
        bel[col]=0;
        if(s[x].size()==1) q.push(x);
    }
    else
    {
        assert(!q.empty());
        int x=q.front();
        assert(s[x].size()<=1);
        push1(col,x),bel[col]=x;
        if(s[x].size()==2) q.pop();
    }
}
int main()
{
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d%d",&n,&m,&k),emp=n,res.clear();
        while(!q.empty()) q.pop();
        for(int i=1;i<=n-1;i++) q.push(i);
        for(int i=1;i<=m;i++) scanf("%d",&a[i]);
        for(int i=1;i<=m;i++)
        {
            if(bel[a[i]]||!q.empty()) work(a[i]);///普通方法足以解决
            else
            {///已经出现n-1个大小2的栈,正在加入一种新的颜色
                int j=i+1;
                while(j<=m&&a[j]!=a[i]&&s[bel[a[j]]].back()==a[j]) j++;
                assert(j<=m);
                if(a[j]==a[i])
                {///在找到下一个栈底之前,找到了和a[i]相同颜色的牌
                    push1(a[i],emp);
                    for(int k=i+1;k<=j-1;k++) work(a[k]);
                    push1(a[j],emp);
                }
                else
                {///j为i之后第一次出现的栈底
                    int x=bel[a[j]],t=s[x].back(),cnt=0;///j所在栈,j为栈底,t为栈顶
                    for(int k=i+1;k<=j-1;k++) cnt+=a[k]==t;///统计栈顶的出现次数
                    if(cnt&1)
                    {///使用策略二
                        push1(a[i],emp);
                        for(int k=i+1;k<=j-1;k++)
                            if(a[k]==t) push1(a[k],x);
                            else work(a[k]);
                        push1(a[j],x);///将x变为空栈,此时emp中有一个元素a[i]
                        bel[a[i]]=emp,bel[a[j]]=bel[t]=0;
                        q.push(emp),emp=x;
                    }
                    else
                    {///使用策略一
                        push1(a[i],x);
                        for(int k=i+1;k<=j-1;k++)
                            if(a[k]==t) push1(a[k],x);
                            else work(a[k]);
                        push1(a[j],emp),push2(x,emp);///此时第x个栈仍有2个元素:t,a[i]
                        bel[a[i]]=x,bel[a[j]]=0;
                    }
                }
                i=j;
            }
        }
        for(int i=1;i<=k;i++) assert(!bel[i]);
        for(int i=1;i<=n;i++) assert(s[i].empty());
        printf("%d\n",res.size());
        for(auto p:res)
            if(!p.se) printf("1 %d\n",p.fi);
            else printf("2 %d %d\n",p.fi,p.se);
    }
    return 0;
}

C.建造军营

题目描述

给定一张 \(n\) 个点, \(m\) 条边的无向连通图。

每个点和每条边均可以独立决策是否被选中,要求至少选中一个点。

求有多少种方案满足,断开任意一条未选中的边,被选中的点仍然连通,对 \(10^9+7\) 取模。

数据范围

  • \(1\le n\le 5\cdot 10^5,n-1\le m\le 10^6\)

时间限制 \(\texttt{1s}\) ,空间限制 \(\texttt{512MB}\)

分析

对于任意一个点集(不能为空,总共 \(2^n-1\) 个),假设有 \(e\) 条桥边,那么这个点集的贡献为 \(2^{m-e}\)

显然同一边双连通分量里的点永远不会被切断。先缩点,则非桥边全部消失,桥边构成一棵树。此后就变成了一个树上问题,只不过点有权值\(w_u\)(即对应的边双中的点数)。

后面很明显要树形\(\text{dp}\),先把状态定出来:

\[f_u=\sum_{以 u 为顶点}\frac1{2^e}\\ g_u=\sum_{包含在 u 子树中的点集,且将顶点到 u 路径全部视为桥边}\frac1{2^e} \]

注意这里点集包含空集,此时当作 \(e=0\) 理解。

先处理 \(g_u\) 的转移。对于每条边 \((u,v)\) ,分情况讨论:

  • \(v\) 子树中不选点,贡献为 \(g_u\)
  • \(v\) 子树中要选点,需要多花一条边 \((u,v)\) 的代价达成目标,同时 \(u\) 子树会贡献完整的\(g_u\),总贡献为 \(\frac12\cdot g_u\cdot(g_v-1)\)

因此 \(g'_u=\frac{g_v+1}2g_u\) ,考虑所有子树的贡献:

\[g_u=2^{w_u}\prod_{v\in son(u)}\frac{g_v+1}2\\ \]

再容斥一下即可算出:

\[f_u=g_u-\sum_{v\in subtree(u)}f_v\cdot\frac1{2^{dis(u,v)}} \]

时间复杂度 \(\mathcal O(n)\)

#include<bits/stdc++.h>
using namespace std;
const int maxn=5e5+5,maxm=2e6+5,mod=1e9+7,inv2=(mod+1)/2;
int m,n,u,v,cnt,res,sum,tot=1;
int head[maxn],to[maxm],nxt[maxm];
vector<int> g[maxn];
int bel[maxn],dfn[maxn],low[maxn];
stack<int> st;
bool vis[maxn];
int pw[2][maxm];
int f[maxn],h[maxn],s[maxn],w[maxn];
void addedge(int u,int v)
{
    nxt[++tot]=head[u],to[tot]=v,head[u]=tot;
}
void tarjan(int u,int fa)
{
    dfn[u]=low[u]=++cnt,st.push(u);
    for(int i=head[u];i;i=nxt[i])
    {
        int v=to[i];
        if(v==fa) continue;
        if(!dfn[v])
        {
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
        }
        else low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        int v;
        sum++;
        do v=st.top(),st.pop(),w[sum]++,bel[v]=sum;
        while(v!=u);
    }
}
void dfs2(int u)
{
    vis[u]=true,f[u]=h[u]=pw[0][w[u]];
    for(auto v:g[u])
    {
        if(vis[v]) continue;
        dfs2(v);
        h[u]=1ll*h[u]*inv2%mod*(h[v]+1)%mod;
        s[u]=(s[u]+1ll*inv2*s[v])%mod;
    }
    f[u]=(h[u]-s[u]+mod)%mod,s[u]=(s[u]+f[u]-1)%mod;
    res=(res+f[u]-1)%mod;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&u,&v);
        addedge(u,v),addedge(v,u);
    }
    tarjan(1,0);
    for(int u=1;u<=n;u++)
        for(int i=head[u];i;i=nxt[i])
        {
            int x=bel[u],y=bel[to[i]];
            if(x==y) continue;
            g[x].push_back(y),g[y].push_back(x);
        }
    pw[0][0]=pw[1][0]=1;
    for(int i=1;i<=m;i++)
    {
        pw[0][i]=2*pw[0][i-1]%mod;
        pw[1][i]=1ll*inv2*pw[1][i-1]%mod;
    }
    dfs2(1);
    printf("%lld\n",1ll*pw[0][m]*(res+mod)%mod);
    return 0;
}

D.比赛

题目描述

给定长为 \(n\) 的排列 \(a,b\)\(q\) 次询问:

\[\sum_{l\le p\le q\le r}\max_{p\le i\le q}a_i\cdot\max_{p\le j\le q}b_j \]

\(2^{64}\) 取模的值。

数据范围

  • \(1\le n,q\le2.5\cdot10^5,1\le l\le r\le n\)

时间限制 \(\texttt{2s}\),空间限制 \(\texttt{512MB}\)

分析

线段树维护区间历史和经典题。

离线扫描线,扫到 \(r\) 时回答所有询问。

对每个 \(l\) ,维护以 \(l\) 为左端点的所有区间的贡献,即 \(\sum_{i=l}^r\max\limits_{l\le j\le i}a_j\cdot\max\limits_{l\le j\le i}b_j\)

单调栈维护到 \(r\) 的后缀 \(\max\) 是容易的,均摊 \(\mathcal O(1)\) 次区间加。

问题转化为:对 \(a\) 区间加、对 \(b\) 区间加、询问区间 \([l,r]\)\(a\cdot b\) 历史和。

推标记可以推到眩晕,用矩乘辅助理解。

\(a\) 区间加操作:

\[\begin{bmatrix}len&a&b&ab&hab\end{bmatrix} \times\begin{bmatrix} 1&v&0&0&0\\ 0&1&0&0&0\\ 0&0&1&v&0\\ 0&0&0&1&0\\ 0&0&0&0&1\\ \end{bmatrix}= \begin{bmatrix}len&a+v\cdot len&b&ab+v\cdot b&hab\end{bmatrix} \]

\(b\) 区间加操作:

\[\begin{bmatrix}len&a&b&ab&hab\end{bmatrix} \times\begin{bmatrix} 1&0&v&0&0\\ 0&1&0&v&0\\ 0&0&1&0&0\\ 0&0&0&1&0\\ 0&0&0&0&1\\ \end{bmatrix}= \begin{bmatrix}len&a&b+v\cdot len&ab+v\cdot a&hab\end{bmatrix} \]

更新历史和:

\[\begin{bmatrix}len&a&b&ab&hab\end{bmatrix} \times\begin{bmatrix} 1&0&0&0&0\\ 0&1&0&0&0\\ 0&0&1&0&0\\ 0&0&0&1&1\\ 0&0&0&0&1\\ \end{bmatrix}= \begin{bmatrix}len&a&b&ab&hab+ab\end{bmatrix} \]

发现转移方向为 \(len\to a,b\to ab\to hab\) ,因此任意时刻转移矩阵一定具有如下形式:

\[\begin{bmatrix} 1&*&*&*&*\\ 0&1&0&*&*\\ 0&0&1&*&*\\ 0&0&0&1&*\\ 0&0&0&0&1\\ \end{bmatrix} \]

维护 \(9\)\(*\) 位置的值即可,时间复杂度\(\mathcal O((n+q)\log n)\)

#include<bits/stdc++.h>
#define ull unsigned long long
#define ls p<<1
#define rs p<<1|1
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=2.5e5+5;
int l,n,q,r;
int a[maxn],b[maxn],st1[maxn],st2[maxn];
ull res[maxn];
vector<pii> g[maxn];
struct vec
{
    ull v[5];
};
struct mat
{
    ull v[9];///(0,1),(0,2),(0,3),(0,4),(1,3),(1,4),(2,3),(2,4),(3,4)
}o,t0,t1,t2;
inline vec operator*(const vec &a,const mat &b)
{
    static vec c;
    c.v[0]=a.v[0];
    c.v[1]=a.v[0]*b.v[0]+a.v[1];
    c.v[2]=a.v[0]*b.v[1]+a.v[2];
    c.v[3]=a.v[0]*b.v[2]+a.v[1]*b.v[4]+a.v[2]*b.v[6]+a.v[3];
    c.v[4]=a.v[0]*b.v[3]+a.v[1]*b.v[5]+a.v[2]*b.v[7]+a.v[3]*b.v[8]+a.v[4];
    return c;
}
inline mat operator*(const mat &a,const mat &b)
{
    static mat c;
    c.v[0]=b.v[0]+a.v[0];
    c.v[1]=b.v[1]+a.v[1];
    c.v[2]=b.v[2]+a.v[0]*b.v[4]+a.v[1]*b.v[6]+a.v[2];
    c.v[3]=b.v[3]+a.v[0]*b.v[5]+a.v[1]*b.v[7]+a.v[2]*b.v[8]+a.v[3];
    c.v[4]=b.v[4]+a.v[4];
    c.v[5]=b.v[5]+a.v[4]*b.v[8]+a.v[5];
    c.v[6]=b.v[6]+a.v[6];
    c.v[7]=b.v[7]+a.v[6]*b.v[8]+a.v[7];
    c.v[8]=a.v[8]+b.v[8];
    return c;
}
struct node
{
    int l,r;
    mat add;
    vec sum;
}f[4*maxn];
inline void pushadd(int p,const mat &v)
{
    f[p].add=f[p].add*v,f[p].sum=f[p].sum*v;
}
inline void pushdown(int p)
{
    pushadd(ls,f[p].add),pushadd(rs,f[p].add),f[p].add=o;
}
inline void pushup(int p)
{
    for(int i=0;i<5;i++) f[p].sum.v[i]=f[ls].sum.v[i]+f[rs].sum.v[i];
}
void build(int p,int l,int r)
{
    f[p].l=l,f[p].r=r;
    if(l==r) return f[p].sum.v[0]=1,void();
    int mid=(l+r)>>1;
    build(ls,l,mid);
    build(rs,mid+1,r);
    pushup(p);
}
void modify(int p,int l,int r,const mat &v)
{
    if(l<=f[p].l&&f[p].r<=r) return pushadd(p,v);
    if(l>f[p].r||r<f[p].l) return ;
    pushdown(p);
    modify(ls,l,r,v);
    modify(rs,l,r,v);
    pushup(p);
}
ull query(int p,int l,int r)
{
    if(l<=f[p].l&&f[p].r<=r) return f[p].sum.v[4];
    if(l>f[p].r||r<f[p].l) return 0;
    pushdown(p);
    return query(ls,l,r)+query(rs,l,r);
}
int main()
{
    scanf("%*d%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    for(int i=1;i<=n;i++) scanf("%d",&b[i]);
    scanf("%d",&q);
    for(int i=1;i<=q;i++) scanf("%d%d",&l,&r),g[r].push_back(mp(l,i));
    build(1,1,n),t0.v[8]=1;
    for(int i=1,top1=0,top2=0;i<=n;i++)
    {
        while(top1&&a[st1[top1]]<=a[i])
        {
            t1.v[0]=t1.v[6]=-a[st1[top1]];
            modify(1,st1[top1-1]+1,st1[top1],t1),top1--;
        }
        while(top2&&b[st2[top2]]<=b[i])
        {
            t2.v[1]=t2.v[4]=-b[st2[top2]];
            modify(1,st2[top2-1]+1,st2[top2],t2),top2--;
        }
        t1.v[0]=t1.v[6]=a[i],modify(1,st1[top1]+1,i,t1),st1[++top1]=i;
        t2.v[1]=t2.v[4]=b[i],modify(1,st2[top2]+1,i,t2),st2[++top2]=i;
        modify(1,1,i,t0);
        for(auto p:g[i]) res[p.se]=query(1,p.fi,i);
    }
    for(int i=1;i<=q;i++) printf("%llu\n",res[i]);
    return 0;
}

posted on 2023-05-19 15:05  peiwenjun  阅读(24)  评论(0)    收藏  举报

导航