【题解】NOIP2022
怎么看 T3 也不是那么难,可是为啥赛时就是被卡死了[难过]
不补 \(B\) 题了,ad-hoc。
A.种花
题目描述:
小 C 决定在他的花园里种出 \(\texttt{CCF}\) 字样的图案,因此他想知道 \(\texttt C\) 和 \(\texttt F\) 两个字母各自有多少种种花的方案;不幸的是,花园中有一些土坑,这些位置无法种花,因此他希望你能帮助他解决这个问题。
花园可以看作有 \(n\times m\) 个位置的网格图,从上到下分别为第 \(1\) 到第 \(n\) 行,从左到右分别为第 \(1\) 列到第 \(m\) 列,其中每个位置有可能是土坑,也有可能不是,可以用 \(a_{i,j} = 1\) 表示第 \(i\) 行第 \(j\) 列这个位置有土坑,否则用 \(a_{i,j} = 0\) 表示这个位置没土坑。
一种种花方案被称为 \(\texttt{C-}\) 形的,如果存在 \(x_1, x_2 \in [1, n]\),以及 \(y_0, y_1, y_2 \in [1, m]\),满足 \(x_1 + 1 < x_2\),并且 \(y_0 < y_1, y_2 \leq m\),使得第 \(x_1\) 行的第 \(y_0\) 到第 \(y_1\) 列、第 \(x_2\) 行的第 \(y_0\) 到第 \(y_2\) 列以及第 \(y_0\) 列的第 \(x_1\) 到第 \(x_2\) 行都不为土坑,且只在上述这些位置上种花。
一种种花方案被称为 \(\texttt{F-}\) 形的,如果存在 \(x_1, x_2, x_3 \in [1, n]\),以及 \(y_0, y_1, y_2 \in [1, m]\),满足 \(x_1 + 1 < x_2 < x_3\),并且 \(y_0 < y_1, y_2 \leq m\),使得第 \(x_1\) 行的第 \(y_0\) 到第 \(y_1\) 列、第 \(x_2\) 行的第 \(y_0\) 到第 \(y_2\) 列以及第 \(y_0\) 列的第 \(x_1\) 到第 \(x_3\) 行都不为土坑,且只在上述这些位置上种花。
样例一解释中给出了 \(\texttt{C-}\) 形和 \(\texttt{F-}\) 形种花方案的图案示例。
现在小 C 想知道,给定 \(n, m\) 以及表示每个位置是否为土坑的值 \(\{a_{i,j}\}\),\(\texttt{C-}\) 形和 \(\texttt{F-}\) 形种花方案分别有多少种可能?由于答案可能非常之大,你只需要输出其对 \(998244353\) 取模的结果即可,具体输出结果请看输出格式部分。
对于所有数据,保证:\(1 \leq T \leq 5\),\(1 \leq n, m \leq 10^3\),\(0 \leq c, f \leq 1\),\(a_{i,j} \in \{0, 1\}\)。
题目分析:
多测不清空直接见祖宗了,绝望。
先考虑统计 \(C\) 再考虑统计 \(F\)。
对于 \(C\),其实就可以拆解为一个横下面加一个 \(L\) 形
所以可以考虑枚举 \(C\) 的左上顶点,这样答案就是下面 \(L\) 形的数量乘以这个点横的数量。
对于 \(L\) 形的数量显然可以使用前缀和快速维护。
对于 \(F\) 可以就是下面的 \(L\) 形变成了一个 \(L\) 形下面加一些竖。
这个东西放到前缀和里只是多乘了一些东西,也十分好维护。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e3+5;
const int MOD = 998244353;
int n,m,C,F;
char s[N][N];
int a[N][N],f[N][N],g[N][N];
int mod(int x){
return ((x % MOD)+MOD)%MOD;
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T,id;
scanf("%lld%lld",&T,&id);
while(T--){
scanf("%lld%lld%lld%lld",&n,&m,&C,&F);
for(int i=1; i<=n; i++) scanf("%s",s[i]+1);
for(int i=n; i>=1; i--){
for(int j=1; j<=m; j++){
if(s[i+1][j] == '0' && s[i][j] == '0') f[i][j] = f[i+1][j];
else if(s[i][j] == '0') f[i][j] = i;
}
}
for(int i=n; i>=1; i--){
for(int j=m; j>=1; j--){
if(s[i][j+1] == '0' && s[i][j] == '0') g[i][j] = g[i][j+1];
else if(s[i][j] == '0') g[i][j] = j;
}
}
for(int i=n; i>=1; i--){
for(int j=m; j>=1; j--){
if(g[i][j] >= j) a[i][j] = mod(g[i][j] - j),a[i][j] = mod(a[i][j] + a[i+1][j]);
}
}
int ans = 0;
//求 C 的方案数
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
if(g[i][j] - j <= 0 || f[i][j] - i < 2) continue;
int cnt1 = mod(g[i][j] - j);
int cnt2 = a[i+2][j];
ans = mod(ans + cnt1 * cnt2);
}
}
printf("%lld ",C * ans);ans = 0;
//求 F 的方案数
for(int i=n; i>=1; i--){
for(int j=m; j>=1; j--){
if(g[i][j] >= j){
a[i][j] = mod((g[i][j] - j) * (f[i][j] - i));
a[i][j] = mod(a[i][j] + a[i+1][j]);
}
}
}
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
if(g[i][j] - j <= 0 || f[i][j] - i < 2) continue;
int cnt1 = mod(g[i][j] - j);
int cnt2 = a[i+2][j];
ans = mod(ans + cnt1 * cnt2);
ans = mod(ans - cnt1 * a[f[i][j]][j]);
}
}
printf("%lld\n",F * ans);ans = 0;
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
f[i][j] = g[i][j] = a[i][j] = 0;
s[i][j] = 'a';
}
}
}
return 0;
}
C.建造军营
题目描述:
A 国与 B 国正在激烈交战中,A 国打算在自己的国土上建造一些军营。
A 国的国土由 \(n\) 座城市组成,\(m\) 条双向道路连接这些城市,使得任意两座城市均可通过道路直接或间接到达。A 国打算选择一座或多座城市(至少一座),并在这些城市上各建造一座军营。
众所周知,军营之间的联络是十分重要的。然而此时 A 国接到情报,B 国将会于不久后袭击 A 国的一条道路,但具体的袭击目标却无从得知。如果 B 国袭击成功,这条道路将被切断,可能会造成 A 国某两个军营无法互相到达,这是 A 国极力避免的。因此 A 国决定派兵看守若干条道路(可以是一条或多条,也可以一条也不看守),A 国有信心保证被派兵看守的道路能够抵御 B 国的袭击而不被切断。
A 国希望制定一个建造军营和看守道路的方案,使得 B 国袭击的无论是 A 国的哪条道路,都不会造成某两座军营无法互相到达。现在,请你帮 A 国计算一下可能的建造军营和看守道路的方案数共有多少。由于方案数可能会很多,你只需要输出其对 \(1,000,000,007\left(10^{9}+7\right)\) 取模的值即可。两个方案被认为是不同的,当且仅当存在至少一 座城市在一个方案中建造了军营而在另一个方案中没有,或者存在至少一条道路在一个 方案中被派兵看守而在另一个方案中没有。
对所有数据,保证 \(1 \leq n \leq 5 \times 10^5\),\(n - 1 \leq m \leq 10^6\),\(1 \leq u_i, v_i \leq n\),\(u_i \neq v_i\)。
题目分析:
注意到每次只可以删掉一条边,也就是说如果两个点满足边双联通那么无论怎么删都不会导致它们无法互相到达。
所以一个显然的想法就是对图进行边双联通分量缩点,这样我们缩点之后其实只需要关注树边就可以了,被缩掉的边选或者不选都无所谓。
可以发现的是如果 \(u,v\) 均建造了兵营,则树上 \(u,v\) 之间的路径必然全部被选择,所以一个显然的状态就是:\(dp[i][0/1][0/1]\) 表示以 \(i\) 为根的子树内,有/没有被选择点,点 \(i\) 有/没有被选择的方案数。
这样需要注意的一点就是如果我们要对 \(dp[i][..][1]\) 转移就必然要求其子树内被选择的点到 \(i\) 的所有边都被选择,但是这个状态的设计下我们完全没有办法处理这个东西,因为我们完全不知道这中间选了多少条边,所以考虑再加一维:\(dp[i][0/1][0/1][0/1]\) 表示以 \(i\) 为根的子树内,有/没有被选择点,点 \(i\) 有/没有被选择,点 \(i\) 子树被选择的点与 \(i\) 的路径上的边是/不是全部被选择的方案数。
这样的话每个点就有八个状态,看上去就超级头大,但是会发现的一点就是其中有实际意义的状态数很少,只有下面这几个:
- \(dp[i][0][0][0/1]\) 表示这棵子树内没有被选择的点
- \(dp[i][1][0][0]\) 代表这棵子树内选择的点与 \(i\) 之间的边没有被全部选择
- \(dp[i][1][1][1]\) 代表这棵子树内选择的点与 \(i\) 之间的边被全部选择,且点 \(i\) 被选择
- \(dp[i][1][0][1]\) 代表这棵子树内选择的点与 \(i\) 之间的边被全部选择,且点 \(i\) 不被选择
其实第三个和第四个可以合并为一个状态,因为显然在后续的转移中如果我们已经要求了边全部被选择,那么显然我们根节点选择或者不选择并不会产生任何影响。
所以总结一下我们只需要记录三个状态,也就是 \(dp[i][3]\),其中:
- \(dp[i][0]\) 代表子树内没选择任何点
- \(dp[i][1]\) 代表子树内选择的点与 \(i\) 之间的边全部被选择
- \(dp[i][2]\) 代表子树内选择的点与 \(i\) 之间的边没有被全部选择
考虑转移其实就是新加入一个子树:
- \(dp[u][0]\),转移显然,乘 \(2\) 的原因是边 \((u,v)\) 可以选择也可以不选择
- \(dp[u][0] = dp[u][0] \times 2 \times dp[v][0]\)
- \(dp[u][1]\),就是考虑这个选择的点是来自于之前的子树,还是来自于新加入的子树
- \(dp[u][1] = dp[u][1] \times (2 \times dp[v][0] + dp[v][1]) + dp[u][0] \times dp[v][1]\)
- \(dp[u][2]\),因为我们显然只能有一棵子树选点,不然不同子树的点之间的边不被选择就不合法了,所以转移就是讨论这个有点的子树,是新加入的这个子树,还是之前加入的子树
- \(dp[u][2] = dp[u][2] \times 2 \times dp[v][0] + dp[u][0] \times (dp[v][1] + 2\times dp[v][2])\)
对 \(dp\) 的初始化其实就是只有 \(u\) 点这一个点情况下的 \(dp\) 值,设 \(szv_u\) 表示 \(u\) 这个边双代表的点的个数,\(sze_u\) 表示 \(u\) 这个边双代表的边的数量,也就是:
- \(dp[u][0] = 2^{sze_u}\)
- \(dp[u][1] = (2^{szv_u} - 1) \times 2^{sze_u}\),减 \(1\) 的原因是根据状态定义至少要选择一个点
- \(dp[u][2] = 0\)
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6+5;
const int MOD = 1e9+7;
struct edge{
int nxt,to;
edge(){}
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
}e[2 * N];
int cnt,head[N],f[N],fa[N],dep[N],sum[N],deg[N],dp[N][4],pos[N],sz_v[N],sz_e[N],pw[N*2],g[N][3],from[N],to[N];
bool vis[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
int find(int x){
if(f[x] == x) return x;
return f[x] = find(f[x]);
}
void merge(int x,int y){
f[find(x)] = find(y);
}
void dfs1(int u,int fath){
vis[u] = true;fa[u] = fath;dep[u] = dep[fath] + 1;
for(int i=head[u]; i; i=e[i].nxt){
int v = e[i].to;
if(vis[v]) continue;
dfs1(v,u);
}
}
void dfs2(int u,int fath){
vis[u] = true;
for(int i=head[u]; i; i=e[i].nxt){
int v = e[i].to;
if(vis[v]) continue;
dfs2(v,u);
sum[u] += sum[v];
}
if(u != 1 && sum[u]) merge(u,fath);
}
void get_dp(int u,int fath){
dp[u][0] = g[u][0];
dp[u][1] = g[u][1];
dp[u][2] = 0;
for(int i=head[u]; i; i=e[i].nxt){
int v = e[i].to;
if(v == fath) continue;
get_dp(v,u);
//注意这个 2 1 0 的顺序
//考虑 f[u][2] 的转移
dp[u][2] = (dp[u][2] * 2 * dp[v][0] + (dp[v][1] + 2 * dp[v][2]) * dp[u][0])%MOD;
//考虑 f[u][1] 的转移
dp[u][1] = (dp[u][0] * dp[v][1] + dp[u][1] * (2 * dp[v][0] + dp[v][1]))%MOD;
//考虑 f[u][0] 的转移
dp[u][0] = dp[u][0] * dp[v][0] * 2 % MOD;
}
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int n,m;scanf("%lld%lld",&n,&m);
for(int i=1; i<=m; i++){
scanf("%lld%lld",&from[i],&to[i]);
add_edge(from[i],to[i]);add_edge(to[i],from[i]);
}
for(int i=1; i<=n; i++) f[i] = i;
dfs1(1,0);
memset(vis,0,sizeof(vis));
for(int i=1; i<=m; i++){
if(fa[from[i]] == to[i] || fa[to[i]] == from[i]) continue;
if(dep[from[i]] < dep[to[i]]) swap(from[i],to[i]);
sum[from[i]]++,sum[to[i]]--;
}
dfs2(1,0);
memset(vis,0,sizeof(vis));
memset(head,0,sizeof(head));cnt = 0;
int sz = 0;
for(int i=1; i<=n; i++){
if(!vis[find(i)]){
pos[find(i)] = ++sz;
vis[find(i)] = true;
}
sz_v[pos[find(i)]]++;
}
for(int i=1; i<=m; i++){
int u = find(from[i]),v = find(to[i]);
if(u == v){
sz_e[pos[u]]++;
}
else{
add_edge(pos[u],pos[v]);
add_edge(pos[v],pos[u]);
deg[pos[u]]++,deg[pos[v]]++;
}
}
// for(int i=1; i<=sz; i++) printf("%lld ",deg[i]);
// printf("\n");
//前面都是在缩边双,下面开始正式写题
pw[0] = 1;
for(int i=1; i<=n+m; i++) pw[i] = pw[i-1] * 2 % MOD;
for(int i=1; i<=sz; i++){
g[i][0] = pw[sz_e[i]];
g[i][1] = (pw[sz_v[i]] - 1) * pw[sz_e[i]] % MOD; //因为不能完全不选
}
get_dp(1,0);
printf("%lld\n",(dp[1][1] + dp[1][2])%MOD);
return 0;
}
D.比赛
题目描述:
小 N 和小 O 会在 2022 年 11 月参加一场盛大的程序设计大赛 NOIP!小 P 会作为裁判主持竞赛。小 N 和小 O 各自率领了一支 \(n\) 个人的队伍,选手在每支队伍内都是从 \(1\) 到 \(n\) 编号。每一个选手都有相应的程序设计水平。具体的,小 N 率领的队伍中,编号为 \(i\)(\(1 \leq i \leq n\))的选手的程序设计水平为 \(a _ i\);小 O 率领的队伍中,编号为 \(i\)(\(1 \leq i \leq n\))的选手的程序设计水平为 \(b _ i\)。特别地,\(\{a _ i\}\) 和 \(\{b _ i\}\) 还分别构成了从 \(1\) 到 \(n\) 的排列。
每场比赛前,考虑到路途距离,选手连续参加比赛等因素,小 P 会选择两个参数 \(l, r\)(\(1 \leq l \leq r \leq n\)),表示这一场比赛会邀请两队中编号属于 \([l, r]\) 的所有选手来到现场准备比赛。在比赛现场,小 N 和小 O 会以掷骰子的方式挑选出参数 \(p, q\)(\(l \leq p \leq q \leq r\)),只有编号属于 \([p, q]\) 的选手才能参赛。为了给观众以最精彩的比赛,两队都会派出编号在 \([p, q]\) 内的、程序设计水平值最大的选手参加比赛。假定小 N 派出的选手水平为 \(m _ a\),小 O 派出的选手水平为 \(m _ b\),则比赛的精彩程度为 \(m _ a \times m _ b\)。
NOIP 总共有 \(Q\) 场比赛,每场比赛的参数 \(l, r\) 都已经确定,但是 \(p, q\) 还没有抽取。小 P 想知道,对于每一场比赛,在其所有可能的 \(p, q\)(\(l \leq p \leq q \leq r\))参数下的比赛的精彩程度之和。由于答案可能非常之大,你只需要对每一场答案输出结果对 \(2 ^ {64}\) 取模的结果即可。
对于所有数据,保证:\(1 \leq n, Q \leq 2.5 \times 10 ^ 5\),\(1 \leq l _ i \leq r _ i \leq n\),\(1 \leq a _ i, b _ i \leq n\) 且 \(\{a _ i\}\) 和 \(\{b _ i\}\) 分别构成了从 \(1\) 到 \(n\) 的排列。
题目分析:
考虑离线,按区间右端点 \(r\) 扫描线。
考虑实时维护 \(X_{l,r},Y_{l,r}\) 分别表示区间 \([l,r]\) 的 \(a\) 以及 \(b\) 的最大值。
每新加入一个区间,就可以使用扫描线维护新加入的点对 \(X,Y\) 的影响,也就是一个区间覆盖操作。
对于区间 \([l,r]\) 的询问,相当于我们扫到 \(r\) 时区间 \([l,r]\) 的历史 \(X \times Y\) 的和,其实也就是:
答案就是 \(\sum_{l' = l}^{r} S_{l',r}\)
总结一下我们就是要支持区间 \(X\) 覆盖,区间 \(Y\) 覆盖,区间 \(S + X \times Y\)。
这个东西就通过线段树打标记可以维护,就是维护 \((s_x,s_y,a_x,a_y,a_{xy},a)\),这个标记的含义就是:
具体的维护就是钦定加法标记先于覆盖标记。
也就是说假设 \(X\) 会被覆盖为 \(s_x\) 则 \(a_{xy}\sum X_iY_i = a_{xy}s_x \sum y_i\),也就是说会让 \(a_{xy}s_x\) 贡献到 \(a_y\) 中。
然后我们就