A - 立方体
题意:给定 N≤200N\le 200N≤200 个轴对齐的立方体(每个用 (x1,y1,z1)(x_1,y_1,z_1)(x1,y1,z1) 到 (x2,y2,z2)(x_2,y_2,z_2)(x2,y2,z2) 表示),它们可能相交或重叠。求这些立方体并集的外表面积。所有坐标在 0,2000,2000,200 内。
赛时没去想这题,后来才发现这题其实是最简单的。
发现数据都 ≤200\le 200≤200,可以求出每个点是否有被立方体所覆盖,然后直接暴力搜索没有被覆盖的点即可。
时间复杂度是 O(2003)O(200^3)O(2003) 的。
cpp
#include<bits/stdc++.h>
using namespace std;
int n,cf[205][205][205],a[205][205][205],ans;
bool vis[205][205][205];
int fx[8][3]={{0,0,1},{0,0,-1},{0,1,0},{0,-1,0},{1,0,0},{-1,0,0}};
queue<pair<int,pair<int,int> > > q;
signed main(){
scanf("%d",&n);
for(int i=1,x_1,y_1,z_1,x_2,y_2,z_2;i<=n;i++){
scanf("%d%d%d%d%d%d",&x_1,&y_1,&z_1,&x_2,&y_2,&z_2);
x_1++,y_1++,z_1++,cf[x_1][y_1][z_1]++,cf[x_2+1][y_1][z_1]--,cf[x_1][y_2+1][z_1]--,cf[x_1][y_1][z_2+1]--,cf[x_2+1][y_2+1][z_1]++,cf[x_2+1][y_1][z_2+1]++,cf[x_1][y_2+1][z_2+1]++,cf[x_2+1][y_2+1][z_2+1]--;
}
for(int i=1;i<=202;i++)
for(int j=1;j<=202;j++)
for(int k=1;k<=202;k++)
a[i][j][k]=cf[i][j][k]+a[i-1][j][k]+a[i][j-1][k]+a[i][j][k-1]-a[i][j-1][k-1]-a[i-1][j][k-1]-a[i-1][j-1][k]+a[i-1][j-1][k-1];
q.push({0,{0,0}}),vis[0][0][0]=1;
while(!q.empty()){
int x=q.front().first,y=q.front().second.first,z=q.front().second.second;
q.pop();
for(int i=0;i<6;i++){
int nx=x+fx[i][0],ny=y+fx[i][1],nz=z+fx[i][2];
if(!(nx>=0&&nx<=202&&ny>=0&&ny<=202&&nz>=0&&nz<=202)) continue;
if(vis[nx][ny][nz]) continue;
if(!a[nx][ny][nz]) q.push({nx,{ny,nz}}),vis[nx][ny][nz]=1;
else ans++;
}
}
return printf("%d\n",ans),0;
}
B - 城市建设
题意:给定一个 nnn 点 mmm 边的无向带权图,qqq 次操作,每次修改一条边的权值。每次修改后,询问当前图的最小生成树边权和。n≤2×104n \le 2\times 10^4n≤2×104,m,q≤5×104m,q \le 5\times 10^4m,q≤5×104。
删边操作比较难处理,考虑离线下来,正难则反,将删边转化成区间含有一条边。
这个容易想到线段树分治,也就是将一段区间拆成 log\loglog 个子区间。
现在,则是需要一种能支持动态 MST 的算法。这个有点类似于 NOI2014 魔法森林,可以使用 LCT,假设原本有一颗 MST,那么加入一条边其实就相当于将连接这两个点当中最大的一条边与新的边比较,看看谁更优则取谁。
cpp
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,q,ans,Cnt;
struct LCT{
int son[2],fa,lazy,Max,val,Max_num;
}tree[10000005];
int identify(int x){
if(tree[tree[x].fa].son[0]==x) return 0;
if(tree[tree[x].fa].son[1]==x) return 1;
return -1;
}
void pushup(int rt){
tree[rt].Max = max(tree[rt].val, max(tree[tree[rt].son[0]].Max, tree[tree[rt].son[1]].Max));
tree[rt].Max_num = 0;
if(tree[rt].Max == tree[rt].val) tree[rt].Max_num = rt;
if(tree[rt].Max == tree[tree[rt].son[0]].Max) tree[rt].Max_num = tree[tree[rt].son[0]].Max_num;
if(tree[rt].Max == tree[tree[rt].son[1]].Max) tree[rt].Max_num = tree[tree[rt].son[1]].Max_num;
}
void rotate(int x){
int y=tree[x].fa,z=tree[y].fa,opx=identify(x),opy=identify(y);
if(opy!=-1) tree[z].son[opy]=x;
tree[x].fa=z;
tree[y].son[opx]=tree[x].son[opx^1],tree[tree[x].son[opx^1]].fa=y;
tree[x].son[opx^1]=y,tree[y].fa=x;
pushup(y),pushup(x);
}
void pushdown(int x){
if(tree[x].lazy){
swap(tree[x].son[0],tree[x].son[1]);
if(tree[x].son[0]) tree[tree[x].son[0]].lazy^=1;
if(tree[x].son[1]) tree[tree[x].son[1]].lazy^=1;
tree[x].lazy=0;
}
}
void pushall(int x){
if(identify(x)!=-1) pushall(tree[x].fa);
pushdown(x);
}
void splay(int x){
pushall(x);
while(identify(x)!=-1){
int y=tree[x].fa,z=tree[y].fa,opx=identify(x),opy=identify(y);
if(opy==-1) rotate(x);
else{
(opx==opy)?rotate(y):rotate(x);
rotate(x);
}
}
}
void access(int x){
for(int p=0;x;p=x,x=tree[x].fa)
splay(x),tree[x].son[1]=p,pushup(x);
}
void makeroot(int x){
access(x),splay(x),tree[x].lazy^=1;
}
int findroot(int x){
access(x),splay(x);
while(tree[x].son[0]) pushdown(x),x=tree[x].son[0];
splay(x);
return x;
}
void link(int x,int y){
makeroot(x);
if(findroot(y)!=x) tree[x].fa=y;
}
void cut(int x, int y) {
makeroot(x),access(y),splay(y);
if(tree[y].son[0] == x && !tree[x].son[1]) {
tree[y].son[0] = tree[x].fa = 0;
pushup(y);
}
}
int x[50005],y[50005],val[50005],lst[50005],X[50005*25],Y[50005*25];
vector<pair<pair<int,int>,int> > v[50005<<2];
void change(int rt,int l,int r,int L,int R,int s,int t,int num){
if(L>R) return;
if(l==L&&r==R){
v[rt].push_back({{s,t},num});
return;
}
int mid=l+r>>1;
if(R<=mid) change(rt<<1,l,mid,L,R,s,t,num);
else if(mid+1<=L) change(rt<<1|1,mid+1,r,L,R,s,t,num);
else change(rt<<1,l,mid,L,mid,s,t,num),change(rt<<1|1,mid+1,r,mid+1,R,s,t,num);
}
stack<pair<int,pair<int,pair<int,int> > > > Stack;
void dfs(int rt,int l,int r){
int sum=0;
for(pair<pair<int,int>,int > i:v[rt]){
int u=i.first.first,v=i.first.second,val=i.second;
if(findroot(u)==findroot(v)){
makeroot(u),access(v),splay(v);
if(tree[v].Max<=val) continue;
ans-=tree[v].Max;
int w=tree[v].Max_num;
Stack.push({-tree[v].Max,{w,{Y[w],X[w]}}}),sum++;
cut(X[w],w),cut(Y[w],w);
}
tree[++Cnt]={{0,0},0,0,val,val,Cnt},link(u,Cnt),link(v,Cnt),Stack.push({val,{Cnt,{u,v}}}),ans+=val,sum++,X[Cnt]=u,Y[Cnt]=v;
}
if(l==r) printf("%lld\n",ans);
else{
int mid=l+r>>1;
dfs(rt<<1,l,mid),dfs(rt<<1|1,mid+1,r);
}
while(sum--){
int op=Stack.top().first,num=Stack.top().second.first,u=Stack.top().second.second.first,v=Stack.top().second.second.second;
Stack.pop(),ans-=op;
if(op<0) link(u,num),link(v,num);
else cut(u,num),cut(v,num);
}
}
signed main(){
scanf("%lld%lld%lld",&n,&m,&q),Cnt=n,tree[0]={{0,0},0,0,-1000000000,-1000000000,0};
for(int i=1;i<=n;i++) tree[i]={{0,0},0,0,-1000000000,-1000000000,i};
for(int i=1;i<=m;i++) scanf("%lld%lld%lld",&x[i],&y[i],&val[i]),lst[i]=1;
for(int i=1,id,Val;i<=q;i++)
scanf("%lld%lld",&id,&Val),change(1,1,q,lst[id],i-1,x[id],y[id],val[id]),val[id]=Val,lst[id]=i;
for(int i=1;i<=m;i++) change(1,1,q,lst[i],q,x[i],y[i],val[i]);
dfs(1,1,q);
return 0;
}
C - 比特矩阵
题意:定义比特矩阵乘法:C=A×BC = A \times BC=A×B 满足 ci,j=⋁k=1n(ai,k⊕bk,j)c_{i,j} = \bigvee_{k=1}^{n} (a_{i,k} \oplus b_{k,j})ci,j=⋁k=1n(ai,k⊕bk,j),其中 ∨\vee∨ 表示按位或,⊕\oplus⊕ 表示按位异或。给定 n×nn \times nn×n 矩阵 AAA 和正整数 mmm(n≤500n \le 500n≤500,m≤109m \le 10^9m≤109),计算 AmA^mAm(mmm 次幂)。矩阵元素为非负整数 ≤109\le 10^9≤109。
二进制拆分,对于每一位考虑。
这样一次矩阵乘法可以用 bitset 优化,降到 O(n3w)O(\frac{n^3}{w})O(wn3),猜一下,肯定是不断循环的,通过暴力,可以发现,循环节很小,在 n=100n=100n=100 时最大循环长度就 555,所以大胆猜测,n=500n=500n=500 时也很小,直接暴力找循环节可能就可以了。
赛时的思路,循环节长度错了。其实随机下很小,但是可以卡到 >n>n>n 的。然后由于数据加强了,就变成 88pts88pts88pts 了。在认为自己做法还是正确的时候,发现了一个将矩阵乘法 O(n3w)O(\frac{n^3}{w})O(wn3) 优化到 O(n2)O(n^2)O(n2) 的做法,就是观察一下,它其实就是等价于判断一行和另一个的一列是否完全相等。如相等,则是 000 否则为 111,用 Hash 即可。
但是,由于循环节的原因,极端情况下是 O(n2loga×循环节)O(n^2 \log a \times 循环节)O(n2loga×循环节) 的,无法通过。
后来,看了题解,其实如果在把矩阵按行在拆一下,问题就解决了。用 n2n^2n2 那个 Hash,可以看出,一个行向量去和那个单位矩阵去做矩阵乘法其实是最多只会有 n+1n+1n+1 种的(与每一个分别相等 + 都不相等)。这样其实问题就解决了,暴力找循环节。但是这里需要提前预处理一下 n+1n+1n+1 种情况之间相互变化关系,用 O(n)O(n)O(n) 求之间变化后的结果(因为是行向量与单位矩阵,所以不用 O(n2)O(n^2)O(n2)),这样就可以降到 O(n2lognloga)O(n^2 \log n \log a)O(n2lognloga) 了。
cpp
#include<bits/stdc++.h>
using namespace std;
int n,k,num[505][505],Ans[505][505],to[2005],Round[2005],to_Round[2005];
bitset<505> Y[505],X[2005],All,s[505];
unordered_map<bitset<505>,int> Map;
bitset<505> ans;
bitset<505> change(bitset<505> x){
ans.reset();
for(int i=1;i<=n;i++) ans[i]=(x!=Y[i]);
return ans;
}
bool vis[505];
vector<int> v[505];
signed main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&num[i][j]);
for(int i=1;i<=n;i++) X[1][i]=1;
for(int w=0;w<=30;w++){
Map.clear();
for(int j=1;j<=n;j++){
Y[j].reset();
for(int i=1;i<=n;i++) Y[j][i]=((num[i][j]>>w)&1);
}
int cnt=2;
Map[X[1]]=1; Map[X[2]]=2;
memset(vis,0,sizeof vis);
for(int i=1;i<=n;i++){
if(vis[i]) continue;
++cnt; X[cnt].reset();
for(int j=1;j<=n;j++)
if(!vis[j] && Y[j]==Y[i])
vis[j]=1, X[cnt][j]=1;
if(Map.count(X[cnt])) cnt--;
else Map[X[cnt]]=cnt;
}
for(int i=1;i<=n;i++){
s[i].reset();
for(int j=1;j<=n;j++) s[i][j]=((num[i][j]>>w)&1);
if(!Map.count(s[i]))
++cnt, X[cnt]=s[i], Map[s[i]]=cnt;
}
queue<int> q;
for(int i=1;i<=cnt;i++) q.push(i);
while(!q.empty()){
int u = q.front(); q.pop();
bitset<505> nxt = change(X[u]);
if(!Map.count(nxt)){
Map[nxt] = ++cnt;
X[cnt] = nxt;
q.push(cnt);
}
}
for(int i=1;i<=cnt;i++) to[i]=Map[change(X[i])];
vector<int> Vis(cnt+1,0), jl_p(cnt+2,0);
for(int i=1;i<=n;i++){
fill(Vis.begin(), Vis.end(), 0);
int state = Map[s[i]];
Vis[state]=1; jl_p[1]=state;
int First=0, Size=0;
int cur = state;
for(int step=2;step<=k;step++){
cur = to[cur];
jl_p[step]=cur;
if(Vis[cur]){
Size = step - Vis[cur];
First = Vis[cur];
break;
}
Vis[cur]=step;
}
int p = k;
if(First) p = (k - First) % Size + First;
for(int j=1;j<=n;j++) if(X[jl_p[p]][j]) Ans[i][j] |= (1<<w);
}
}
for(int i=1;i<=n;i++,puts(""))
for(int j=1;j<=n;j++)
printf("%d ",Ans[i][j]);
return 0;
}
总结
对于像 B 这样删边操作很难处理而又没强制在线的时候,可以想一想反向来做,将它转化成一段区间内的加边,用线段树分治将它分解,从而使其问题转化容易。
而 C 这样,需要多挖掘一下题目的性质,而不是盲目的用暴力去验证自己的猜想。