前缀和
https://atcoder.jp/contests/abc434/tasks/abc434_d
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int main(){
vector<vector<ll>>a(2025,vector<ll>(2025,0));
vector<vector<ll>>b(2025,vector<ll>(2025,0));
ll n;cin>>n;
for(int i=1;i<=n;i++){
ll u,d,l,r;cin>>u>>d>>l>>r;
d++;r++;
a[u][l]++;a[u][r]--;a[d][l]--;a[d][r]++;
b[u][l]+=i;b[u][r]-=i;b[d][l]-=i;b[d][r]+=i;
}for(int i=0;i<2025;i++){
for(int j=0;j<2025;j++){
if(j){a[i][j]+=a[i][j-1];
b[i][j]+=b[i][j-1];}
}
}for(int i=0;i<2025;i++){
for(int j=0;j<2025;j++){
if(i){a[i][j]+=a[i-1][j];
b[i][j]+=b[i-1][j];}
}
}
vector<ll>bk(n+1,0);
for(int i=1;i<=2000;i++){
for(int j=1;j<=2000;j++){
if(a[i][j]==0){bk[0]++;}
else if(a[i][j]==1){bk[b[i][j]]++;}//记录是第几块
}
}for(int i=1;i<=n;i++){
cout<<bk[0]+bk[i]<<'\n';
}
}
核心思想:动态规划
前缀和的计算本质上是动态规划,利用已经计算好的结果来推导新的结果。
分步证明
第一步:水平方向前缀和
执行完第一个双重循环后:
a[i][j] = 原数组第i行中,从第0列到第j列的所有元素之和
即:a[i][j] = original[i][0] + original[i][1] + ... + original[i][j]
第二步:垂直方向前缀和
现在我们要计算从 (0,0) 到 (i,j) 的矩形区域和:
目标:sum(0,0 → i,j) = 所有行从第0列到第j列的和之和
当执行 a[i][j] += a[i-1][j] 时:
- 左边的
a[i][j]是第i行从0到j列的和 - 右边的
a[i-1][j]是前i-1行从0到j列的总和(因为上一步已经计算了水平前缀和)
所以:
a[i][j] = (第i行从0到j列的和) + (前i-1行从0到j列的总和)
= 从第0行到第i行,每行从0列到j列的和之和
= 从(0,0)到(i,j)的矩形区域和
可视化例子
假设原数组:
1 2 3
4 5 6
7 8 9
第一步后(水平前缀和):
1 3 6
4 9 15
7 15 24
第二步后(垂直前缀和):
1 3 6
5 12 21 ← 1+4=5, 3+9=12, 6+15=21
12 27 45 ← 5+7=12, 12+15=27, 21+24=45
验证:a[2][2] = 45 正是原数组所有元素之和 1+2+3+4+5+6+7+8+9=45
为什么顺序很重要
如果调换两个循环的顺序(先垂直后水平),结果也是正确的,因为前缀和计算满足交换律。
这种分步计算的方法比直接四重循环更高效,时间复杂度从 O(n⁴) 降到了 O(n²)。
这是一个很好的问题!让我来解释为什么树结构要减1。
dfs
https://atcoder.jp/contests/abc434/tasks/abc434_e
cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n;cin>>n;
vector<int>x(n),r(n);
map<int,vector<pair<int,int>>>g;
for(int i=0;i<n;i++){
cin>>x[i]>>r[i];
int xp=x[i]+r[i];
int xr=x[i]-r[i];
g[xr].emplace_back(xp,i);
g[xp].emplace_back(xr,i);
//TODO
}set<int>vis;
auto dfs=[&](auto rc,int c,int en)->pair<int,int>{
vis.insert(c);//标记当前坐标已访问
int num=1;//当前联通分量的节点数
int is=true;
for(auto& [d,e]:g[c]){
if(e==en)continue;
if(vis.count(d)){//邻接点已访问,存在环,不是树
is=false;
}else{
auto[n,f]=rc(rc,d,e);
num+=n;
is&=f;
}
}return make_pair(num,is);//节点数,是否是树
};
int ans=0;
for(auto& [ket,_]:g){
if(vis.count(ket))continue;
//对每个联通分量dfs
auto[num,is]=dfs(dfs,ket,-1);
//是树,不同坐标=节点-1
//是环,不同坐标=节点
ans+=is?num-1:num;
//TODO
}cout<<ans<<'\n';
}
一棵树第一个点两条边,后来多一个点一条边,所以是n-1
如果有环,先最小生成树有n-1个点,后来利用其他边来涂色这个
模拟、思维
https://codeforces.com/contest/2158/problem/D
cpp
#include <bits/stdc++.h>
using namespace std;
vector<array<int,2>>reduce(string &s){
int n=s.size();
vector<array<int,2>>op;
int p=-1;
//寻找相邻相同
for(int i=0;i<n-1;i++){
if(s[i]==s[i+1])p=i;//相邻字符的位置
//TODO
}//无相同,严格交替
if(p==-1){
//前3个一定是回文
op.push_back({0,2});
s[0]^=1;s[1]^=1;
p=2;//现在有两位相等
}char va=s[p+1];//块中字符值
int l=p,r=p+1;//【l,r】中字符相等
//向右扩展
while(r+1<n){
if(s[r+1]!=va)op.push_back({l,r});//字符不同,翻转
r++;va=s[r];//翻转后这一段都变成s【r】
}while(l){//字符不同,翻转
if(s[l-1]!=va)op.push_back({l,r});
l--;va=s[l];//更新当前块的值
}
//全1翻转变全0
if(va=='1')op.push_back({0,n-1});
return op;
}
void solve(){
int n;cin>>n;
string s,t;cin>>s>>t;
auto ops=reduce(s);//s转到全0
auto opt=reduce(t);//t转到全0
//t变全0转,将全0变t
reverse(opt.begin(),opt.end());
int mo=ops.size()+opt.size();
cout<<mo<<'\n';
//s到全0,全0到t
for(auto &[x,y]:ops){cout<<x+1<<' '<<y+1<<'\n';
}for(auto &[x,y]:opt){cout<<x+1<<' '<<y+1<<'\n';
}
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t;cin>>t;
while(t--){
solve();
}
}