P3434 [POI2006] KRA-The Disks
考场上不知道在想什么,把 \(O(n)\) 正解改成 \(O(n \mathrm{log}n)\) 的了。
关于 \(O(n\mathrm{log}n)\) 做法很多,我只讲我的。直接二分盘子会在哪里卡住,二分范围是 \(1\sim lst\)。\(lst\) 表示上一个盘子卡住的位置。
\(\mathrm{Code}\)
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int n,m,lst,k;
int st[20][N],lg[N];
int read(){char ch=getchar();int x=0,f=1;while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;
}
inline void ST(){for(int i=2;i<=n;++i)lg[i]=lg[i>>1]+1;for(int i=1;i<=lg[n];++i){for(int j=1;j<=n-(1<<i)+1;++j){st[i][j]=min(st[i-1][j],st[i-1][j+(1<<(i-1))]);}}
}
inline int Qry(int l,int r){int p=lg[r-l+1];return min(st[p][l],st[p][r-(1<<p)+1]);
}
inline bool chk(int x){return Qry(1,x)>=k;
}
int main(){freopen("kra.in","r",stdin);freopen("kra.out","w",stdout);n=read(),m=read();for(int i=1;i<=n;++i)st[0][i]=read();ST();lst=n+1;int l,r,mid;for(int i=1;i<=m;++i){k=read();l=0,r=lst-1;while(l<r){mid=(l+r+1)>>1;if(chk(mid))l=mid;else r=mid-1;}if(!l){printf("0");return 0;}else lst=l;}printf("%d",lst);return 0;
}
\(O(n)\) 正解就是注意到盘子位置是单调递减的,合法的位置是要满足 \(r_i>=k \ \to f_i>=k\) 所以我们直接维护一个单调的前缀最小,从下向上扫,只要满足 \(f_i \le k\) 就停止扫描。实现有些细节要注意。
\(\mathrm{Code}\)
#include<bits/sdc++.h>
using namespace std;
const int N=3e5+5;
int n,m,k,r[N];
int read(){char ch=getchar();int x=0,f=1;while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;
}
int main(){n=read(),m=read();r[0]=1e9+1;for(int i=1;i<=n;++i)r[i]=read(),r[i]=min(r[i-1],r[i]);int lst=n+1;for(int i=1;i<=m;++i){k=read();while(r[lst]<k) --lst;--lst;if(lst==0){printf("0");return 0;}}printf("%d",lst+1);return 0;
}
P3459 [POI2007] MEG-Megalopolis
树剖秒了。
\(\mathrm{Code}\)
#include<bits/stdc++.h>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int N=1e5+5;
int n,m;
int f[N],hs[N],sz[N],top[N],dfn[N],dep[N];
struct E{int to,nxt;
}e[N<<1];int hd[N],tot,idx;
inline void Add(int u,int v){e[++tot].to=v,e[tot].nxt=hd[u],hd[u]=tot;
}
struct Tr{int l,r,val,tag;
}tr[N<<2];
int read(){char ch=getchar();int x=0,f=1;while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;
}
inline void dfs1(int nw,int fa){f[nw]=fa;sz[nw]=1;dep[nw]=dep[fa]+1;for(int i=hd[nw];i;i=e[i].nxt){if(e[i].to!=fa){dfs1(e[i].to,nw);sz[nw]+=sz[e[i].to];if(sz[e[i].to]>sz[hs[nw]]) hs[nw]=e[i].to;}}
}
inline void dfs2(int nw,int fa){dfn[nw]=++idx;top[nw]=fa;if(!hs[nw])return;dfs2(hs[nw],fa);for(int i=hd[nw];i;i=e[i].nxt){if(e[i].to!=f[nw]&&e[i].to!=hs[nw]){dfs2(e[i].to,e[i].to);}}
}
inline void Build(const int p,const int l,const int r){tr[p].l=l,tr[p].r=r;if(l==r){if(l!=1)tr[p].val=1;return;}int mid=(l+r)>>1;Build(ls,l,mid),Build(rs,mid+1,r);tr[p].val=tr[ls].val+tr[rs].val;
}
inline void Change(const int p,const int val){tr[p].val-=(tr[p].r-tr[p].l+1)*val;tr[p].tag+=val;
}
inline void Pushdown(const int p){if(!tr[p].tag)return;Change(ls,tr[p].tag),Change(rs,tr[p].tag);tr[p].tag=0;
}
inline void Mdf(const int p,const int l,const int r){if(tr[p].l>=l&&tr[p].r<=r){Change(p,1);return;}Pushdown(p);int mid=(tr[p].l+tr[p].r)>>1;if(l<=mid)Mdf(ls,l,r);if(mid<r)Mdf(rs,l,r);tr[p].val=tr[ls].val+tr[rs].val;
}
inline int Qry(const int p,const int l,const int r){if(tr[p].l>=l&&tr[p].r<=r) return tr[p].val;Pushdown(p);int mid=(tr[p].l+tr[p].r)>>1,res=0;if(l<=mid)res+=Qry(ls,l,r);if(mid<r)res+=Qry(rs,l,r);return res;
}
inline int Path_Q(int v){int res=0;while(top[v]!=1){res+=Qry(1,dfn[top[v]],dfn[v]);v=f[top[v]];}return res+Qry(1,1,dfn[v]);
}
int main(){freopen("meg.in","r",stdin);freopen("meg.out","w",stdout);n=read();int u,v;char id;for(int i=1;i<n;++i)u=read(),v=read(),Add(u,v),Add(v,u);dfs1(1,0),dfs2(1,1);Build(1,1,n);m=read();for(int i=1;i<=n+m-1;++i){id=getchar();if(id=='A'){u=read(),v=read(); Mdf(1,dfn[v],dfn[v]);}else{v=read();printf("%d\n",Path_Q(v));}}return 0;
}
/*
5
1 2
1 3
1 4
4 5
4
W 5
A 1 4
W 5
A 4 5
W 5
W 2
A 1 2
A 1 3*/
可以不树剖,只借用树剖的思想,把dfn序拉出来,原问题相当于是子树修改单点查询前缀,或者单点修改区间查询,树状数组维护即可。(其实我一开始是这么想的,但是树剖秒了)
或者括号序列。
结论:\(i\) 入栈时栈内的点是 \(1\) 到 \(i\) 的路径上的点。
以 \(1\) 为根进行 \(\mathrm{DFS}\) ,每个点(1除外)入栈时向序列中加一个 \(1\) ,出栈时加入一个 \(-1\) (其实不就是差分吗)。
对于更改 \(E(s,t)\) ,我们将较深点对应的权值变为 \(0\) ;对于询问,设 \(x\) 入栈位置为 \(K\) ,那么 \(1\) 到 \(x\) 的距离为前 \(K\) 个元素的和。
使用树状数组维护即可。
同学的 \(\mathrm{Code}\)
#include<bits/stdc++.h>
using namespace std;
int n,m;
vector<int> g[250025];
int fa[250025],dep[250025],dfn[250025],all,size[250025];
int cf[250025];
void dfs(int s){size[dfn[s]]=1;for(int i=0;i<g[s].size();i++){int to=g[s][i];if(dfn[to]) continue;dfn[to]=++all; fa[all]=dfn[s];dep[all]=dep[dfn[s]]+1;dfs(to);size[dfn[s]]+=size[dfn[to]];}
}
int d[250025];
int lowbit(int x){return x&(-x);
}
void add(int p,int num){while(p<=n){d[p]+=num;p+=lowbit(p);}
}
int getnum(int p){int sum=0;while(p){sum+=d[p];p-=lowbit(p);}return sum;
}
void build(int l,int r){for(int i=1;i<=n;i++) cf[i]=dep[i]-dep[i-1];for(int i=1;i<=n;i++) add(i,cf[i]);
}
int main(){freopen("meg.in","r",stdin);freopen("meg.out","w",stdout);ios::sync_with_stdio(0);cin>>n;for(int i=1;i<n;i++){int u,v;cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}dep[1]=0; dfn[1]=++all;dfs(1);build(1,n);cin>>m;int T=n+m-1;while(T--){char op;cin>>op;if(op=='A'){int a,b;cin>>a>>b;if(dfn[a]<dfn[b]) swap(a,b);add(dfn[a],-1);add(dfn[a]+size[dfn[a]],1);}else{int a,t;int ans=0;cin>>a;cout<<getnum(dfn[a])<<'\n';}} return 0;
}
P3457 [POI2007] POW-The Flood 题解
gs题面。你说得对,但是直接判连通块有 20 分,输出 1 甚至有 30 分。
先说明一下抽水的定义:
假设当前有一个抽水机放在 \(i\) 点,若存在一条到 \(j\) 的路径 \(P\)(不包含 \(i,j\) )使得 \(\small \max\limits_{k\in P} h_k \le h_j\),则 \(j\) 点也可以被抽干。每个点的高度是其对应位置数的绝对值。这一点后面重要声明会用到。
非常直接的想法就是选择最低的地方放抽水机,但是在什么地方放呢?
我们是要在抽干城市的水,所以只在城市放是不劣的,这一点很多题解没有说清楚。
考虑反证法,如果有一个低的点 可以使得放在这里 能让这个连通块内的城市全都被抽干,那么一定存在一个城市也可以,因为一定有一个路径 可以通过低的那个点走到其他城市;而如果对于高的点,放抽水机肯定不优。感性理解一下
其实这个可以看一下这篇题解,不是一定放在城市,只是可以。
那么我们的步骤就出来了:
我们将点从小到大排序,并对这个点进行合并操作,维护连通块。如果这个点所在连通块中没有城市且这个点是城市,就放一个抽水机。
一个非常重要的事情我们要申明:
连通块存在的意义是判断当前点的水能不能流到前面判过了的海拔较低的点,是判断一个连通块内的点都可以走到最低的点,而我们的实现是反着的,是判断这个点能不能被周围的点到达。因为我们是从前往后扫,只知道小的点的联通情况,所以这个点与前面的点是被到达的关系(BFS 是正着的)!我们正着思考是如我上面所说的抽水定义来判断的,反着思考虽然因为对称性(应该不叫对称性,姑且一用)是一样的,但是本质上是有区别的。更通俗一点就是我们是根据水能不能从当前点流到最低点来判断能不能被低的点抽到,是判断当前点能否被代表元到达,而不是以当前点为代表元,那样就成了高的点抽低的点的水了。
还有一个问题:
由于每次每个点只会在周围四个点向外合并,所以如果一个点被所有相同的海拔包在里面了(虽然高度相同,但是由于排序了,所以不会在一个地方),并查集是会把它们判成不同连通块的的。是出于这个问题才应该将所有海拔相同的合并完,再返回去处理前面的。Hack 数据我借用一下第二篇题解的:
3(1) 3(2) 2(3) 1(4)
括号表示编号。
排完序变成
1 2 3(1) 3(2)
我们从前往后扫,发现第一个 \(3\) 和后面的 \(1,2\) 中间是有一个 \(3\) 隔着的,这个 \(3\) 没有更新。
为什么要这样做,是因为手太短了,一次搜索没有将所有可以并的点都并进来,传递性出了问题,而BFS就没有这个问题。
至于为什么这样是对的,如果两个海拔相同的所在的连通块隔开没有影响,如果两个海拔相同的连通块在一起那么可以合并,也没有影响。
我们可以发现,只有我提到的被相同的包住这种情况会产生,因为如果高了就出不去,低了之前就被合并过。所以不是因为海拔相同出问题,是因为只有海拔相同这个问题才会暴露出来。
细节见代码。
这个题的本质见 另一篇题解。
\(\mathrm{Code}\)
并查集 好久没写过这么不压行的代码了
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
struct P{int x,y,h;
}p[N];
int n,m,f[1005][1005];
int fa[N],tag[N],ans;
inline int read(){char ch=getchar();int x=0,f=1;while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;
}
inline bool cmp(P A,P B){return abs(A.h)<abs(B.h);}
inline int pos(int x,int y){return m*(x-1)+y;}
inline int find(int x){if(fa[x]==x)return x;else return fa[x]=find(fa[x]);
}
inline void merge(int x,int y){x=find(x),y=find(y);if(x==y) return;fa[y]=x,tag[x]|=tag[y];//从哪里到,就是哪里的儿子
}
int main(){n=read(),m=read();for(int i=0;i<=n+1;++i){for(int j=0;j<=m+1;++j){f[i][j]=1e9+5;}}for(int i=1;i<=n;++i){for(int j=1;j<=m;++j){p[pos(i,j)].x=i,p[pos(i,j)].y=j;f[i][j]=p[pos(i,j)].h=read();}}sort(p+1,p+n*m+1,cmp);for(int i=1;i<=n*m;++i) fa[i]=i;for(int i=1;i<=n*m;++i){if(abs(p[i].h)>=abs(f[p[i].x-1][p[i].y])){merge(pos(p[i].x,p[i].y),pos(p[i].x-1,p[i].y));}if(abs(p[i].h)>=abs(f[p[i].x+1][p[i].y])){merge(pos(p[i].x,p[i].y),pos(p[i].x+1,p[i].y));}if(abs(p[i].h)>=abs(f[p[i].x][p[i].y-1])){merge(pos(p[i].x,p[i].y),pos(p[i].x,p[i].y-1));}if(abs(p[i].h)>=abs(f[p[i].x][p[i].y+1])){merge(pos(p[i].x,p[i].y),pos(p[i].x,p[i].y+1));}if(abs(p[i].h)==abs(p[i+1].h))continue;int y;for(int j=i;abs(p[j].h)==abs(p[i].h);--j){//对之前所有海拔相同的点都统计 if(f[p[j].x][p[j].y]>0){y=find(pos(p[j].x,p[j].y));if(!tag[y]) tag[y]=1,++ans;}}}printf("%d",ans);return 0;
}
BFS(同学的)
#include<bits/stdc++.h>
using namespace std;
int n,m,ans=0,sum,all;
int hy[4]={-1,0,1,0};
int zy[4]={0,1,0,-1};
int a[1010][1010];
int num[1010][1010];
int b[1010][1010];
bool book[1010][1010];
struct node{int x,y,dat;
};
bool cmp(node t,node q){return t.dat<q.dat;
}
vector<node> temp;
void bfs(int x,int y){deque<node> q;q.push_front((node){x,y,num[x][y]});while(!q.empty()){node top=q.front(); q.pop_front();book[top.x][top.y]=0;for(int i=0;i<4;i++){int nx=top.x+hy[i];int ny=top.y+zy[i];if(nx<1||ny<1||nx>n||ny>m) continue;if(a[nx][ny]>0&&num[top.x][top.y]>a[nx][ny]) continue;int h=max(b[nx][ny],num[top.x][top.y]);if(num[nx][ny]>h){num[nx][ny]=h;if(!book[nx][ny]){book[nx][ny]=1;if(num[nx][ny]<=num[top.x][top.y]){q.push_front((node){nx,ny,num[nx][ny]});}else q.push_back((node){nx,ny,num[nx][ny]});}}}}
}
int main(){
// freopen("pow.in","r",stdin);
// freopen("pow.out","w",stdout);ios::sync_with_stdio(0);cin>>n>>m;for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){cin>>a[i][j];if(a[i][j]>0){temp.push_back((node){i,j,a[i][j]});}b[i][j]=abs(a[i][j]);}}memset(num,0x3f,sizeof(num));sort(temp.begin(),temp.end(),cmp);for(int i=0;i<temp.size();i++){int x=temp[i].x,y=temp[i].y,h=temp[i].dat;if(num[x][y]<=h) continue;ans++; num[x][y]=h;bfs(x,y);}cout<<ans;return 0;
}
P3444 [POI2006] ORK-Ploughing
首先可以记搜,然后可以随机化贪心,然后可以退火。
这个贪心确实难想。
考虑最后的形态必然是只剩一行或一列,那么这启发我们以行为主或以列为主来贪心。
这相当于为我们规定了一个优先级,在这个优先级下的贪心才是正确的,因为和最后的子结构是吻合的。
那我们两个都做一遍,是类似的,下面以行优先为例。
那么考虑我们的贪心策略应该是先把行删完,那么我们在过程中一定不能删太多的列,这样不符合结构,对答案不优。或者说,我们一次性删列,把列留到一起删是最优的(我们答案下界正是这样的,只删行或者只删列,这样看起来就是非常优美的)。
但是总有删列的时候,所以我们枚举删多少列,然后每次按照 “上下左右” 的顺序删,取最小则是对的。
具体证明看 Alex_wei 的题解。
\(\mathrm{Code}\)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e3+5;
int n,m,K,fi[N][N],suml[N][N],sumr[N][N];
int ans=1e9+1;
int read(){char ch=getchar();int x=0,f=1;while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*f;
}
int main(){K=read(),m=read(),n=read();for(int i=1;i<=n;++i){for(int j=1;j<=m;++j)fi[i][j]=read();}for(int i=1;i<=n;++i){for(int j=1;j<=m;++j)sumr[i][j]=sumr[i][j-1]+fi[i][j];}for(int i=1;i<=m;++i){for(int j=1;j<=n;++j)suml[j][i]=suml[j-1][i]+fi[j][i];}for(int i=1;i<=m;++i){int lx=1,ly=1,rx=n,ry=m,ans1=0;while(lx<=rx&&ly<=ry){++ans1;if(sumr[lx][ry]-sumr[lx][ly-1]<=K){++lx;continue;}if(sumr[rx][ry]-sumr[rx][ly-1]<=K){--rx;continue;}if(suml[rx][ly]-suml[lx-1][ly]<=K&&ly<i){++ly;continue;}if(suml[rx][ry]-suml[lx-1][ry]<=K){--ry;continue;} ans1=1e9+1;break;}ans=min(ans,ans1);}for(int i=1;i<=n;++i){int lx=1,ly=1,rx=n,ry=m,ans2=0;while(lx<=rx&&ly<=ry){++ans2;if(suml[rx][ly]-suml[lx-1][ly]<=K){++ly;continue;}if(suml[rx][ry]-suml[lx-1][ry]<=K){--ry;continue;} if(sumr[lx][ry]-sumr[lx][ly-1]<=K&&lx<i){++lx;continue;}if(sumr[rx][ry]-sumr[rx][ly-1]<=K){--rx;continue;}ans2=1e9+1;break;}ans=min(ans,ans2);}printf("%d",ans);return 0;
}