NAIPC 2017 部分题解 (CDGH)
C. Stretching Streamers【dp思路+状态设计】
可以把gcd先预处理一下,然后就变成了Codeforces 888F,即如何在圆上$n$个点进行划分、使得划分线不相交。
根据$1$和$n$是否相连,分为两种情况。
当$1$和$n$相连时,考虑枚举最大的一个分界点$i$,要求$i+1...n$不会有边连向$1...i$。故可以划分成两个子问题,即划分$1...i$($1$与$i$可能相连)和划分$i+1...n$($i+1$与$n$可能相连)。
当$1$和$n$不相连时,考虑枚举最大的一个与$1$相连的点$i$。此时$i+1...n$必然有点连向$i$,故可以划分成两个子问题,即划分$1...i$($1$与$i$必须相连)和划分$i...n$($i$与$n$可能相连)。
于是用$dp[l][r][0/1]$表示,划分区间$[l,r]$、且$l,r$是否必须相连时的方案数。就有$dp[l][r][0]=\sum_{mid} dp[l][mid][0/1]\cdot dp[mid+1][r][0/1]$,$dp[l][r][1]=\sum_{mid} dp[l][mid][1]\cdot dp[mid][r][0/1]$。注意连边是存在限制的。
时间复杂度:$O(n^3)$
(下面是Codeforces 888F的代码)
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=505; const int mod=1000000007; int n; int a[N][N]; ll dp[N][N][2]; void dfs(int l,int r) { if(dp[l][r][0]!=-1) return; dp[l][r][0]=dp[l][r][1]=0; for(int i=l;i<r;i++) { ll L,R; dfs(l,i),dfs(i,r),dfs(i+1,r); L=a[l][i]?dp[l][i][1]:0; R=dp[i][r][0]+(a[i][r]?dp[i][r][1]:0); dp[l][r][0]=(dp[l][r][0]+L*R)%mod; L=dp[l][i][0]+(a[l][i]?dp[l][i][1]:0); R=dp[i+1][r][0]+(a[i+1][r]?dp[i+1][r][1]:0); dp[l][r][1]=(dp[l][r][1]+L*R)%mod; } } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&a[i][j]); memset(dp,-1,sizeof(dp)); for(int i=1;i<=n;i++) { dp[i][i][0]=1,dp[i][i][1]=0; for(int j=1;j<i;j++) dp[i][j][0]=dp[i][j][1]=0; } dfs(1,n); ll ans=dp[1][n][0]+(a[1][n]?dp[1][n][1]:0); printf("%lld\n",ans%mod); return 0; }
D. Heaps from Trees【性质+启发式合并】
首先考虑树是一条链的情况,就相当于求LIS,所以算法的下界至少为$O(n\cdot logn)$。
考虑有什么求LIS的办法。其中一个是线段树,显然不太好扩展到树上。另一种是二分+类似栈的结构。
利用二分求LIS是这个思路:我们用一个类似栈的数据结构维护 LIS=$i$的子串结尾 的最小值。那么我们就可以在栈中二分找到 最后一个小于当前值的值,而这个值的下标就是 以当前值为结尾的最大LIS长度 再减$1$。得到了以当前值为结尾的最大LIS长度后,我们尝试更新栈,即取$stack[current\_LIS]=min(current\_LIS, current\_val)$,或当$current\_val$比所有数都大时直接插入在栈的最后。
现在考虑树上是什么情况。对于一条链,我们可以用相同的方法进行维护;对于一个有多个儿子的节点,我们需要将儿子所在的栈进行合并。通过观察,可以发现将多个栈进行合并的结果,就是将栈中的所有元素拿出来整体排序。于是可以用multiset来维护栈,而合并多个multiset的时候采用启发式合并。由于将儿子的multiset拷贝到当前节点复杂度会爆炸,所以本质上是开一个set数组、每个点对应一个set的下标,从而避免拷贝。最终结果为合并到根节点的multiset大小。
时间复杂度:$O(n\cdot (logn)^2)$
#include <set> #include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; const int N=200005; int n; int a[N],p[N]; vector<int> v[N]; int id[N]; multiset<int> s[N]; void dfs(int x) { for(int y: v[x]) { dfs(y); if(s[id[x]].size()<s[id[y]].size()) id[x]=id[y]; } if(!id[x]) id[x]=x; for(int y: v[x]) if(id[y]!=id[x]) for(int tmp: s[id[y]]) s[id[x]].insert(tmp); auto pos=s[id[x]].lower_bound(a[x]); if(pos!=s[id[x]].end()) s[id[x]].erase(pos); s[id[x]].insert(a[x]); } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d%d",&a[i],&p[i]); v[p[i]].emplace_back(i); } dfs(1); printf("%d\n",(int)s[id[1]].size()); return 0; }
E. Blazing New Trails【套路:带权二分】
G. Apple Market【套路:ST表优化建图】
最大流是逃不了的,但是建图需要设计一下。
裸的建图中,所有商店有$n\times n$个节点,每个节点向汇点连流量为$a[i][j]$的边;所有人共有$q$个节点,每个节点由源点流入流量$x_i$,并且连向$(down-up+1)\times (right-left+1)$个商店、流量为INF。那么此时共有$n\times n+q$个节点 和 $n\times n+n\times n\times q$条边。
现在我们考虑适当增加点数,从而减少$q$个节点所连的边数。
假如我们用类似二维线段树的结构,再构造$O(n^2)$级别的点,使得上层节点流入下层节点、线段树叶节点为单独的商店。由于我们在二维线段树上查询矩形需要$(logn)^2$个节点,所以建边能够优化为$n\times n+(log n)^2\times q$。不过这还不够。
由于我们不会对于图的结构进行任何修改,所以不妨考虑采用ST表结构。我们用$4$个参数表示一个节点$x,y,i,j$:$x,y$表示一个矩形区域左上角的坐标,$i,j$表示这个矩形在$x,y$方向的长度分别为$2^i,2^j$。一个节点$node[x][i][y][j]$可以连向$4$个节点:$node[x][i-1][y][j]$,$node[x+(1<<i-1)][i-1][y][j]$,$node[x][i][y][i-1]$,$node[x][i][y+(1<<i-1)][j-1]$,即分别是沿$x,y$方向对半切的子矩形。一维ST表覆盖区间只需要两个子区间,类似的,二维ST表覆盖一个矩形只需要$4$个子矩形。故此时总点数为$(n\cdot logn)^2+q$,总边数为$4(n\cdot logn)^2+4q$。
由于原图是二分图,故优化建图后跑网络流复杂度依然是$O(n\sqrt{n})$。
复杂度:$O(x\sqrt{x})$,其中$x=(n\cdot logn)^2+q$
#include <queue> #include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; //注意N是总点数 //有可能cap/flow超int 那么注意INF要重新设置 const int N=200005; const ll INF=1LL<<60; struct edge { ll cap; int to,rev; edge(int x,ll y,int z) { to=x,cap=y,rev=z; } }; int s,t; vector<edge> v[N]; inline void add_edge(int x,int y,ll cap) { v[x].push_back(edge(y,cap,v[y].size())); v[y].push_back(edge(x,0,v[x].size()-1)); } int level[N]; queue<int> Q; void bfs() { memset(level,-1,sizeof(level)); level[s]=0; Q.push(s); while(!Q.empty()) { int x=Q.front(); Q.pop(); for(int i=0;i<v[x].size();i++) { edge &e=v[x][i]; if(e.cap>0 && level[e.to]<0) { level[e.to]=level[x]+1; Q.push(e.to); } } } } int iter[N]; ll dfs(int x,ll f) { if(x==t) return f; for(int &i=iter[x];i<v[x].size();i++) { edge &e=v[x][i]; if(e.cap>0 && level[e.to]>level[x]) { ll d=dfs(e.to,min(f,e.cap)); if(d>0) { e.cap-=d; v[e.to][e.rev].cap+=d; return d; } } } return 0; } ll max_flow() { ll flow=0; while(1) { bfs(); if(level[t]<0) break; memset(iter,0,sizeof(iter)); ll f=0; while((f=dfs(s,INF))>0) flow+=f; } return flow; } int n,m,q; int tot,id[100][6][100][6]; int main() { scanf("%d%d%d",&n,&m,&q); for(int i=0;(1<<i)<=n;i++) for(int j=0;(1<<j)<=m;j++) for(int x=1;x<=n;x++) for(int y=1;y<=m;y++) { id[x][i][y][j]=++tot; if(i==0 && j==0) continue; if(i==0) { add_edge(tot,id[x][i][y][j-1],INF); add_edge(tot,id[x][i][y+(1<<j-1)][j-1],INF); continue; } if(j==0) { add_edge(tot,id[x][i-1][y][j],INF); add_edge(tot,id[x+(1<<i-1)][i-1][y][j],INF); continue; } add_edge(tot,id[x][i-1][y][j-1],INF); add_edge(tot,id[x+(1<<i-1)][i-1][y][j-1],INF); add_edge(tot,id[x][i-1][y+(1<<j-1)][j-1],INF); add_edge(tot,id[x+(1<<i-1)][i-1][y+(1<<j-1)][j-1],INF); } s=++tot,t=++tot; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) { int x; scanf("%d",&x); add_edge((i-1)*m+j,t,x); } while(q--) { int u,d,l,r,x; scanf("%d%d%d%d%d",&u,&d,&l,&r,&x); add_edge(s,++tot,x); int x_log=0,y_log=0; while((1<<x_log+1)<=d-u+1) x_log++; while((1<<y_log+1)<=r-l+1) y_log++; add_edge(tot,id[u][x_log][l][y_log],INF); add_edge(tot,id[d-(1<<x_log)+1][x_log][l][y_log],INF); add_edge(tot,id[u][x_log][r-(1<<y_log)+1][y_log],INF); add_edge(tot,id[d-(1<<x_log)+1][x_log][r-(1<<y_log)+1][y_log],INF); } printf("%lld\n",max_flow()); return 0; }
H. Maximum Color Clique(性质+dp状态设计)
需要能看出一个性质:因为“任意环中至少存在一对相邻同色边”,所以图中必然存在一个点,其连接的所有边颜色相同。
利用反证法,假如不存在这样的一个点,我们从任意一点出发,每次走与上条边颜色不同的边(因为根据假设,每个点至少存在两种不同颜色的边),那么一直走下去一定能走到一个点,其与起点的边与第一条边(或路径上出现过的某个点)的颜色不同。
我们将这个点和与其相连的边从图中删去,那么剩下的图中仍然存在一个这样的点(因为“任意环中至少存在一条相邻同色边”的条件不会改变),一直这样删可以得到一个序列。我们可以用 每个点颜色全相同的出边颜色 来作为每个点的tag。我们在序列上任意选一个点集$S$,$S$中颜色为$c$的subset大小就是$S$中点tag为$c$的点的数量(不算$S$中下标最大的点)。
我们按照$S$中最后一个下标 从小到大的顺序进行dp。问题就变成了,共有$300$个颜色,每个颜色有$cnt_i$个节点,我们从中最多选择$x$个同颜色节点的选择方案数。可以考虑依次对于每个颜色均进行一次背包。假设当前枚举的节点颜色为$color$,那么枚举$1...color-1$中最多选择同颜色节点的数量为$pre\_mx$、再枚举在当前颜色中选取$cur\_sel$个点。此时能进行一次转移$new\_dp[max(pre\_mx,cur\_sel+1)]+=dp[pre\_mx]\cdot \begin{pmatrix}cnt[color]\\ cur\_sel\end{pmatrix}$。对于$cur\_sel+1$是因为下标最大 的点可以属于任何一个颜色的subset。
这样看起来要进行$4$个for循环嵌套,但是实际上$\sum_{color} cnt[color]$是不超过$n$的,所以其实是$O(n^3)$的一个dp。rls的dp思路比其余ac代码的要清晰很多。
时间复杂度:$O(n^3)$
#include <queue> #include <cstdio> #include <vector> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int N=305; const int mod=1000000007; ll C[N][N]; int n; int a[N][N]; int cnt[N],deg[N][N]; inline void inc(int x,int c) { if(++deg[x][c]==1) cnt[x]++; } inline void dec(int x,int c) { if(--deg[x][c]==0) cnt[x]--; } int vis[N],ord[N],edge[N]; ll dp[N],tmp[N]; inline void add(ll &dest,ll dlt) { dest=(dest+dlt)%mod; } int main() { for(int i=0;i<N;i++) C[i][0]=C[i][i]=1; for(int i=1;i<N;i++) for(int j=1;j<i;j++) C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod; scanf("%d",&n); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { scanf("%d",&a[i][j]); if(i!=j && i<j) inc(i,a[i][j]),inc(j,a[i][j]); } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(!vis[j] && cnt[j]==1) { vis[j]=1,ord[i]=j; for(int k=1;k<=n;k++) if(!vis[k]) dec(k,a[j][k]),edge[i]=a[j][k]; break; } for(int i=1;i<=300;i++) cnt[i]=0; dp[1]=1; ll ans=0; for(int i=1;i<=n;i++) { for(int j=1;j<=i;j++) add(ans,j*dp[j]); memset(dp,0,sizeof(dp)); dp[1]=1; cnt[edge[i]]++; for(int color=1;color<=300;color++) { memcpy(tmp,dp,sizeof(dp)); memset(dp,0,sizeof(dp)); for(int pre_mx=1;pre_mx<=i+1;pre_mx++) for(int cur_sel=0;cur_sel<=cnt[color];cur_sel++) add(dp[max(pre_mx,cur_sel+1)],tmp[pre_mx]*C[cnt[color]][cur_sel]); } } printf("%lld\n",ans); return 0; }

浙公网安备 33010602011771号