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\) 的栈和一个空栈),并且出现了一种新的颜色。
我们根据牌堆中的下一张牌来定策略:
- 如果下一张牌的同类牌在栈底,把牌堆顶放到这个栈中即可。
注意代码实现时不需要考虑这类情况,因为可以被合并到下一类中 \(cnt\) 为偶数的部分。
- 如果下一张牌的同类牌在栈顶,又该怎么办呢?
策略一:找到下一次出现同类牌在栈底的位置,然后放到相应的栈中。
下图就是一个很好的例子:
但这并不足以解决本题,如果出现下面这种情况,策略一就失效了:
但是对于这种情况,如果把第一张牌放入空栈,好像又可以了!
策略二:将牌堆顶放入空栈。
但是如何决策应该使用哪个策略?
记后面第一次出现同类牌在栈底的位置为\(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}\),先把状态定出来:
注意这里点集包含空集,此时当作 \(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\) ,考虑所有子树的贡献:
再容斥一下即可算出:
时间复杂度 \(\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\) 次询问:
对 \(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\) 区间加操作:
对 \(b\) 区间加操作:
更新历史和:
发现转移方向为 \(len\to a,b\to ab\to hab\) ,因此任意时刻转移矩阵一定具有如下形式:
维护 \(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;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/17415188.html
浙公网安备 33010602011771号