7.6
NWERC 2016
A. Arranging Hat
\(f[i][j]\) 表示前 \(i\) 个串,总共修改了 \(j\) 次,得到的最小值是多少。
转移就枚举一个 \(k\),实现一个 \(work(a_{i+1},f_{i,j},k)\) 表示将 \(a_{i+1}\) 经过不超过 \(k\) 次修改,得到的大于等于 \(f_{i,j}\) 的最小字典序
这部分是一个情况稍多的贪心,想清楚细节。
还有一个问题是,前 \(4\) 位可以确定 \(10^4\) 个串的大小关系,所以 DP 数组的第二维只需要开到 \(4n\) 即可。
#include <bits/stdc++.h>
using namespace std;
const int N=45,M=405;
#define ll long long
int n,m;
struct Node{
char s[M];
bool gg;
void print(){
for (int i=1;i<=m;i++) putchar(s[i]);
puts("");
}
};
Node a[N],dp[N][205];
int lst[N][205];
bool check(Node a,Node b){
for (int i=1;i<=m;i++){
if (a.s[i]==b.s[i]) continue;
return a.s[i]<b.s[i];
}
return true;
}
Node work(Node lst,Node now,int kk){
if (kk==0){
if (check(lst,now)) {
now.gg=false;
return now;
}
now.gg=true;
return now;
}
bool flag=false;int k=kk;
Node tmp=now;int high=1;
for (int i=1;i<=m&&k;i++)
if (tmp.s[i]!=lst.s[i]){
tmp.s[i]=lst.s[i];
k--;high=i;
}
if (check(lst,tmp)) {
tmp.gg=false;
return tmp;
}
bool all_9=true;
for (int i=1;i<=high;i++)
if (tmp.s[i]!='9') {
all_9=false;
break;
}
if (all_9){
tmp.gg=true;
return tmp;
}
k=kk;tmp=now;
static bool vis[M];
for (int i=1;i<=m;i++) vis[i]=false;
for (int i=1;i<=m&&k;i++)
if (tmp.s[i]!=lst.s[i]){
tmp.s[i]=lst.s[i];
k--;vis[i]=true;
}
for (int i=high;i>=1;i--){
if (tmp.s[i]=='9') continue;
k=-1;
for (int j=i;j<=n;j++) if (vis[j]) k++;
tmp.s[i]=lst.s[i]+1;
if (tmp.s[i]==now.s[i]) k++;
for (int j=i+1;j<=n;j++){
if (k>0){
tmp.s[j]='0';
if (now.s[j]!='0') k--;
}else tmp.s[j]=now.s[j];
}
break;
}
tmp.gg=false;
return tmp;
}
int ans[N];
int main(){
// freopen ("a.in","r",stdin);
scanf ("%d%d",&n,&m);
for (int i=1;i<=n;i++) scanf ("%s",a[i].s+1);
for (int i=1;i<=m;i++) a[0].s[i]='0';
for (int i=0;i<=n;i++)
for (int j=0;j<=n*4;j++)
dp[i][j].gg=true;
dp[0][0]=a[0];
for (int i=0;i<n;i++){
for (int j=0;j<=min(i*m,n*4);j++)
if (!dp[i][j].gg){
for (int k=0;k<=m;k++){
if (j+k>n*4) break;
Node tmp=work(dp[i][j],a[i+1],k);
if (tmp.gg==true) continue;
if (dp[i+1][j+k].gg==true) dp[i+1][j+k]=tmp,lst[i+1][j+k]=j;
else if (check(tmp,dp[i+1][j+k])){
dp[i+1][j+k]=tmp;
lst[i+1][j+k]=j;
}
}
}
}
int pos=-1;
for (int i=0;i<=n*4;i++)
if (!dp[n][i].gg) {
pos=i;
break;
}
for (int i=n;i>=1;i--) ans[i]=pos,pos=lst[i][pos];
for (int i=1;i<=n;i++) dp[i][ans[i]].print();
return 0;
}
B. British Menu
由于强连通分量大小不超过 5,把它们缩起来,然后暴力求出同一个分量中两两之间的距离
在缩点后建立的新图上跑拓扑序 dp 求最长路,\(dis[x][i]\) 表示停留在原图第 \(i\) 个点上的最长路长度,转移即可。
细节较多
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5,E=1e6+5;
int Head[N],Next[E],Adj[E],Id1[E],Id2[E],tot=0;
void addedge(int u,int v,int id1=0,int id2=0){
Next[++tot]=Head[u],Head[u]=tot,Adj[tot]=v;
Id1[tot]=id1,Id2[tot]=id2;
}
int dfn[N],low[N],Time=0,s[N],top=0;
bool vis[N];int bel[N],cnt=0;
int to[N];
struct Node{
vector <int> p;
bool e[5][5];
int dis[5][5];
void dfs(int f,int x,int l){
vis[x]=true;dis[f][x]=max(dis[f][x],l);
for (int i=0;i<p.size();i++)
if (e[x][i]&&!vis[i])
dfs(f,i,l+1);
vis[x]=false;
}
void calc_dist(){
if (p.size()==1) return;
for (int i=0;i<p.size();i++) dfs(i,i,0);
}
}w[N];
void Tarjan(int x){
dfn[x]=low[x]=++Time;
s[++top]=x;vis[x]=true;
for (int e=Head[x];e;e=Next[e])
if (!dfn[Adj[e]]) Tarjan(Adj[e]),low[x]=min(low[x],low[Adj[e]]);
else if (vis[Adj[e]]) low[x]=min(low[x],dfn[Adj[e]]);
if (dfn[x]==low[x]){
cnt++;
while (s[top+1]!=x){
bel[s[top]]=cnt;
to[s[top]]=w[cnt].p.size();
w[cnt].p.emplace_back(x);
vis[s[top--]]=false;
}
}
}
int u[E],v[E],in[N];
int dis[N][5];
int main(){
int n,m;scanf ("%d%d",&n,&m);
for (int i=1;i<=m;i++) {
scanf ("%d%d",&u[i],&v[i]);
addedge(u[i],v[i]);
}
for (int i=1;i<=n;i++) if (!dfn[i]) Tarjan(i);
memset (Head,0,sizeof(Head));
memset (Next,0,sizeof(Next));
memset (Adj,0,sizeof(Adj));tot=0;
memset (vis,0,sizeof(vis));
for (int i=1;i<=m;i++)
if (bel[u[i]]==bel[v[i]])
w[bel[u[i]]].e[to[u[i]]][to[v[i]]]=true;
for (int i=1;i<=cnt;i++)
w[i].calc_dist();
for (int i=1;i<=m;i++)
if (bel[u[i]]!=bel[v[i]])
addedge(bel[u[i]],bel[v[i]],to[u[i]],to[v[i]]),in[bel[v[i]]]++;
queue <int> Q;
for (int i=1;i<=cnt;i++) if (!in[i]) Q.push(i);
int ans=0;
while (!Q.empty()){
int x=Q.front();Q.pop();
for (int e=Head[x];e;e=Next[e]) {
in[Adj[e]]--;int mx=0;
if (in[Adj[e]]==0) Q.push(Adj[e]);
for (int i=0;i<w[x].p.size();i++) mx=max(mx,w[x].dis[i][Id1[e]]+dis[x][i]);
dis[Adj[e]][Id2[e]]=max(dis[Adj[e]][Id2[e]],mx+1);
}
for (int i=0;i<w[x].p.size();i++) ans=max(ans,dis[x][i]+(int)w[x].p.size()-1);
}
printf ("%d",ans+1);
return 0;
}
J Jupiter Orbiter
拆点最大流判定,很显然的建图,没什么好说的
#include <bits/stdc++.h>
using namespace std;
const int N=10005,E=200005,INF=1e9;
int Head[N],Next[E],Adj[E],Flow[E],tot=1;
inline void addedge(int u,int v,int w){
Next[++tot]=Head[u],Head[u]=tot,Adj[tot]=v,Flow[tot]=w;
Next[++tot]=Head[v],Head[v]=tot,Adj[tot]=u,Flow[tot]=0;
}
int S,T,level[N];
int Q[N];
bool bfs(){
memset (level,-1,sizeof(level));
int l=1,r=0;
Q[++r]=S,level[S]=0;
while (l<=r){
int x=Q[l++];
for (int e=Head[x];e;e=Next[e])
if (level[Adj[e]]==-1&&Flow[e])
level[Adj[e]]=level[x]+1,Q[++r]=Adj[e];
}
return level[T]!=-1;
}
int dfs(int x,int flow){
if (x==T||!flow) return flow;
int ret=0,c;
for (int e=Head[x];e;e=Next[e])
if (level[Adj[e]]==level[x]+1&&Flow[e]&&(c=dfs(Adj[e],min(flow-ret,Flow[e])))){
ret+=c;Flow[e]-=c,Flow[e^1]+=c;
if (ret==flow) break;
}
if (!ret) level[x]=-1;
return ret;
}
int to[N],c[N];
int n,q,s;
int ID(int i,int j){
return n+2+i*q+j;
}
int ID2(int i,int j){
return n+2+q*n+i*q+j;
}
int main(){
scanf ("%d%d%d",&n,&q,&s);
for (int i=1;i<=s;i++) scanf ("%d",&to[i]);
for (int i=1;i<=q;i++) scanf ("%d",&c[i]);
int id=0;S=++id,T=++id;
for (int i=1;i<=n;i++){
int d;scanf ("%d",&d);
++id;addedge(id,T,d);
for (int j=1,a;j<=s;j++){
scanf ("%d",&a);
addedge(S,ID(i,to[j]),a);
}
for (int j=1;j<=q;j++) {
addedge(ID(i,j),ID2(i,j),c[j]);
addedge(ID2(i,j),id,INF);
}
if (i!=1){
for (int j=1;j<=q;j++)
addedge(ID2(i-1,j),ID(i,j),c[j]);
}
}
while (bfs()) dfs(S,INF);
bool flag=true;
for (int e=Head[S];e;e=Next[e])
if (Flow[e]) flag=false;
puts(flag?"possible":"impossible");
return 0;
}
7.5
NWERC 2018
A. Access Points
很容易发现横纵坐标独立,可以做两个一维的,答案加起来。
结论:如果一段 \(x_i\dots x_j\) 是确定的,那么最小化 \(\sum_{k=j}^i(a-x_k)^2\) 时,\(a=\frac{1}{j-i+1}\sum_{k=j}^ix_k\)
现在,我们假设原序列被分成了若干段相同的,那么当前段如果不符合递增的要求,就不断向前合并,使用单调栈维护这个过程
#include <bits/stdc++.h>
using namespace std;
#define ld long double
const int N=100005;
ld x[N],y[N];int n;
ld ans=0;
struct Node{
int l,r;ld val;
}s[N];
Node merge(Node p,Node q){
return (Node){min(p.l,q.l),max(p.r,q.r),(p.val*(p.r-p.l+1)+q.val*(q.r-q.l+1))/(p.r-p.l+1+q.r-q.l+1)};
}
void solve(ld*a){
int top=0;
for (int i=1;i<=n;i++){
Node now=(Node){i,i,a[i]};
while (top&&now.val<s[top].val){
now=merge(now,s[top]);
--top;
}
s[++top]=now;
}
static ld b[N];
for (int i=1;i<=top;i++)
for (int j=s[i].l;j<=s[i].r;j++)
b[j]=s[i].val;
for (int i=1;i<=n;i++) ans+=(a[i]-b[i])*(a[i]-b[i]);
}
int main(){
scanf ("%d",&n);
for (int i=1;i<=n;i++) scanf ("%Lf%Lf",&x[i],&y[i]);
solve(x);
solve(y);
printf ("%.8Lf",ans);
return 0;
}
F. Fastest Speedrun
好题。
先把所有的 \(a[i][j]\) 减去 \(s[i]\),然后把 \(s[i]\) 累加到答案去,使秘密路径变成免费的。
之后按照 \(x[i]\to i\) 连边,形成了一个基环树森林。
我们可以发现,如果打掉一个环上某一个点,那么可以顺序用 \(0\) 的代价打掉整个连通块,而又可以观察发现最优解一定是通过某种方式快速拿到 \(n\) 武器,再打掉剩下的。
所以我们把环上所有点的 \(a[i][j]\) 减去这个环最小的 \(a[i][n]\) ,跑 \(0\to n\) 的最短路即可。一个点在最短路上,说明它是在获得 \(n\) 的过程中需要打掉的,否则表示它是回头再来打的。
时间复杂度 \(O(n^2)\)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2505;
int x[N],s[N],a[N][N];
vector <int> vec[N];
int n,m=0;
bool vis[N];
void dfs(int x){
vis[x]=true;
for (int i=0;i<vec[x].size();i++)
if (!vis[vec[x][i]]) dfs(vec[x][i]);
}
ll dis[N];
int e[N][N];
int main(){
long long S=0;scanf ("%d",&n);
for (int i=1;i<=n;i++){
scanf ("%d%d",&x[i],&s[i]);
for (int j=0;j<=n;j++) scanf ("%d",&a[i][j]),a[i][j]-=s[i];
S+=s[i];vec[x[i]].emplace_back(i);
}
dfs(0);int cnt=0;
for (int i=1;i<=n;i++){
if (vis[i]) continue;
int t=i;bool flag=false;
for (int j=1;j<=n;j++) {
t=x[t];
if (t==i) {
flag=true;
break;
}
}
if (!flag) continue;
vis[i]=true;
t=x[i];m=a[i][n];
while (t!=i){
vis[t]=true;
m=min(m,a[t][n]);
t=x[t];
}
t=x[i];
for (int j=0;j<=n;j++) a[i][j]-=m;
while (t!=i){
for (int j=0;j<=n;j++) a[t][j]-=m;
t=x[t];
}
S+=m;
}
memset (e,0x3f,sizeof(e));
for (int i=0;i<=n;i++)
for (int j=1;j<=n;j++)
e[i][j]=a[j][i];
for (int i=1;i<=n;i++) e[x[i]][i]=0;
memset (dis,0x3f,sizeof(dis));
memset (vis,false,sizeof(vis));
dis[0]=0;
for (int t=1;t<=n;t++){
int mn=-1;
for (int i=0;i<=n;i++)
if (!vis[i]&&(mn==-1||dis[i]<dis[mn])) mn=i;
vis[mn]=true;
for (int i=0;i<=n;i++)
dis[i]=min(dis[i],dis[mn]+e[mn][i]);
}
printf ("%lld",dis[n]+S);
return 0;
}
7.4
看了下牛客挑战赛 61 的题
B “经典”问题
纯随机数据,期望 mex 很小,暴力即可。
C 维护序列
差分,每个位置矩阵快速幂
D 平衡
考虑每次找到最高点,bfs 更新是一个不会形成环的过程,二分答案 check
但是这样是 \(\log^2 n\) 的,发现权值很小,可以用桶记录最高点的高度,省去堆的复杂度。
#include <bits/stdc++.h>
using namespace std;
const int N=500005;
vector <int> a[N],b[N];
int n,m,k,mx;
vector < pair<int,int> > w[N];
const int dx[]={0,1,0,-1,1,-1},dy[]={1,0,-1,0,-1,1};
bool check(int lim){
for (int i=0;i<=1000;i++) w[i].clear();
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
b[i][j]=a[i][j];
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
if (b[i][j]>=0) w[b[i][j]].emplace_back(make_pair(i,j));
int tot=0;
for (int i=mx;i>=0;i--){
for (int j=0;j<w[i].size();j++){
int x=w[i][j].first,y=w[i][j].second;
if (b[x][y]==-2) continue;
for (int k=0;k<6;k++){
int xx=x+dx[k],yy=y+dy[k];
if (xx<1||xx>n||yy<1||yy>m) continue;
if (b[xx][yy]<0) continue;
if (b[xx][yy]<b[x][y]-lim){
tot+=b[x][y]-lim-b[xx][yy];
b[xx][yy]=b[x][y]-lim;
}
w[b[xx][yy]].emplace_back(make_pair(xx,yy));
}
b[x][y]=-2;
}
}
return tot<=k;
}
int main(){
scanf ("%d%d%d",&n,&m,&k);
for (int i=1;i<=n;i++) a[i].resize(m+1),b[i].resize(m+1);
for (int i=1;i<=n;i++)
for (int j=1;j<=m;j++)
scanf ("%d",&a[i][j]),mx=max(mx,a[i][j]);
int l=0,r=1000,ans=1000;
while (l<=r){
int mid=(l+r)>>1;
if (check(mid)) ans=mid,r=mid-1;
else l=mid+1;
}
printf ("%d",ans);
return 0;
}
E 刀虎二象性
对每个下标和每种权值分别建一个点,下表并到权值上去
前两个查询正常做就行,最后一种查询把时间记录在边上,并查集按秩合并
#include <bits/stdc++.h>
using namespace std;
const int N=1000005;
int fa[N],tim[N],to[N];
int a[N];
map <int,int> m,siz,dep;
int getrt(int x){
while (fa[x]) x=fa[x];
return x;
}
int lca(int u,int v){
if (getrt(u)!=getrt(v)) return -1;
int now=u,d1=0,d2=0,mx=0;
while (now) now=fa[now],d1++;
now=v;
while (now) now=fa[now],d2++;
while (d1>d2) --d1,mx=max(mx,tim[u]),u=fa[u];
while (d1<d2) --d2,mx=max(mx,tim[v]),v=fa[v];
if (u==v) return mx;
while (u!=v) {
mx=max(mx,max(tim[u],tim[v]));
u=fa[u],v=fa[v];
}
return mx;
}
int main(){
int T,cnt=0,n=0,ans=0;scanf ("%d",&T);
for (int t=1;t<=T;t++){
int opt;scanf ("%d",&opt);
if (opt==1){
int x;scanf ("%d",&x);
x^=ans;
a[++n]=++cnt;
if (!m[x]) {
m[x]=++cnt;
dep[x]=0,siz[x]=1;
to[cnt]=x;
}else siz[x]++;
fa[a[n]]=m[x];
tim[a[n]]=t;
}else if (opt==2){
int x,y;scanf ("%d%d",&x,&y);
x^=ans,y^=ans;
if (x==y) continue;
if (!m[x]) continue;
if (!m[y]) {
m[y]=++cnt;
to[cnt]=y,dep[y]=0,siz[y]=0;
}
if (dep[y]<dep[x]) {
to[m[x]]=y;
swap(dep[x],dep[y]);
swap(m[x],m[y]);
}
siz[y]+=siz[x];
dep[y]=max(dep[y],dep[x]+1);
fa[m[x]]=m[y];
tim[m[x]]=t;
to[m[x]]=0;siz[x]=m[x]=dep[x]=0;
}else if (opt==3){
int x;scanf ("%d",&x);
x^=ans;
printf ("%d\n",ans=siz[x]);
}else if (opt==4){
int x;scanf ("%d",&x);
x^=ans;
printf ("%d\n",ans=to[getrt(a[x])]);
}else{
int x,y;scanf ("%d%d",&x,&y);
x^=ans,y^=ans;
if (x==y) printf ("%d\n",ans=tim[a[x]]);
else {
int p=lca(a[x],a[y]);
if (p==-1) puts("-1");
else printf ("%d\n",ans=p);
}
}
}
return 0;
}
7.3
补一下 Codeforces Global Round 21
C. Fishingprince Plays With Array
考虑全都拆分到不能再拆分,这样的表示是唯一的,那么都拆分再比较是否完全相同即可
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int a[N],b[N];
long long c1[N],c2[N];
int main(){
int T;scanf ("%d",&T);
while (T--){
int n,m;scanf ("%d%d",&n,&m);
for (int i=1;i<=n;i++) {
scanf ("%d",&a[i]);
c1[i]=1;
while (a[i]%m==0){
a[i]/=m;
c1[i]*=m;
}
if (a[i]==a[i-1]) {
c1[i-1]+=c1[i];
i--;n--;
}
}
int nn;scanf ("%d",&nn);
for (int i=1;i<=nn;i++) {
scanf ("%d",&b[i]);
c2[i]=1;
while (b[i]%m==0){
b[i]/=m;
c2[i]*=m;
}
if (b[i]==b[i-1]) {
c2[i-1]+=c2[i];
i--;nn--;
}
}
if (n!=nn) puts("No");
else{
bool flag=true;
for (int i=1;i<=n;i++)
if (a[i]!=b[i]||c1[i]!=c2[i]) {
flag=false;break;
}
puts(flag?"Yes":"No");
}
}
return 0;
}
D. Permutation Graph
按顺序考虑,令 \(f[i]\) 为 \(1\) 到 \(i\) 的答案,计算 \(f[i]\) 可以由哪些 \(j\) \((j<i)\) 转移过来。
我们可以通过 \(a[i]\) 和 \(a[i-1]\) 的大小关系确定 \(i\) 的转移是 \([\max,\min]\) 还是 \([\min,\max]\) 。
证明一个猜想:只要找最前一个可以转移的端点,就可以保证这个转移是最优的。
考虑所有可以转移的位置,由于左端点是 \(\max\) 还是 \(\min\) 已经确定下来了,这些位置的值必然是单调的,因此全部跨过用一次转移是不劣的。
所以只要用单调栈维护所有可能的 \(\max,\min\) 端点即可。
#include <bits/stdc++.h>
using namespace std;
const int N=5e5+5;
int a[N],f[N];
int s1[N],t1,s2[N],t2;
int setmx(int x){
for (int i=t2;i>=1;i--)
if (a[s2[i]]>a[x]) return s2[i]+1;
return 1;
}
int setmn(int x){
for (int i=t1;i>=1;i--)
if (a[s1[i]]<a[x]) return s1[i]+1;
return 1;
}
int main(){
int T;scanf ("%d",&T);
while (T--){
int n;scanf ("%d",&n);
for (int i=0;i<=n;i++) f[i]=1e9;
for (int i=1;i<=n;i++) scanf ("%d",&a[i]);
f[1]=0;int mnpos=1,mxpos=1;
s1[t1=1]=s2[t2=1]=1;
for (int i=2;i<=n;i++){
f[i]=f[i-1]+1;
if (a[i]>a[i-1]){
int Lbound=setmx(i);
int L=lower_bound(s1+1,s1+t1+1,Lbound)-s1;
f[i]=min(f[i],f[s1[L]]+1);
}else{
int Lbound=setmn(i);
int L=lower_bound(s2+1,s2+t2+1,Lbound)-s2;
f[i]=min(f[i],f[s2[L]]+1);
}
while (t1&&a[s1[t1]]>a[i]) --t1;
while (t2&&a[s2[t2]]<a[i]) --t2;
s1[++t1]=i,s2[++t2]=i;
}
printf ("%d\n",f[n]);
}
return 0;
}
E. Placing Jinas
可以发现递推式就是杨辉三角,所以第 \(i\) 行第 \(j\) 列的方案数是 \(\binom{i+j}{i}\)
现在是一堆这个东西求和,那么枚举行,快速计算一列的和即可。
计算一列的和用到了组合恒等式:
组合意义是考虑 \(n+1\) 个位置里面,选择 \(k+1\) 个位置,扔掉第一个位置以及它前面的所有,在后面剩下的就是选 \(k\) 个的方案数。
#include <bits/stdc++.h>
using namespace std;
const int N=500005,Mod=1e9+7;
inline int qpow(int a,int b){
int ans=1;
while (b){
if (b&1) ans=1ll*ans*a%Mod;
a=1ll*a*a%Mod;b>>=1;
}
return ans;
}
int a[N],fac[N],inv[N];
int C(int n,int m){
if (n<0||m<0||n<m) return 0;
return 1ll*fac[n]*inv[m]%Mod*inv[n-m]%Mod;
}
int main(){
int n;scanf ("%d",&n);fac[0]=1;
for (int i=1;i<=500000;i++) fac[i]=1ll*fac[i-1]*i%Mod;
inv[500000]=qpow(fac[500000],Mod-2);
for (int i=499999;i>=0;i--) inv[i]=1ll*inv[i+1]*(i+1)%Mod;
int ans=0;
for (int i=0;i<=n;i++){
int a;scanf ("%d",&a);
if (a==0) break;
ans=(ans+C(a+i,i+1))%Mod;
}
printf ("%d",ans);
return 0;
}
F. Tree Recovery
很妙的题。我们可以知道,如果树中存在相邻的三个点 \(x,y,z\) ,\((x,y)\) 和 \((y,z)\) 是树中的边,那么 \(d(y,x)=d(y,z)\) 一定存在,这是显然的。
但是,上面的情况仅表示距离为 \(1\) 的情况,距离为 \(2\) 或更大会对这个性质产生干扰。
我们考虑这样的做法:把一个点对看成点,相邻且距离相等的点对之间连边,建立新图。那么距离为 \(1\) 的那种情况建立出来的子图一定不会和其他部分相交。所以遍历每个连通块 check 即可。
check 的次数不超过 \(n\) 次,时间复杂度为 \(O(n^4)\) ,可以 bitset 优化为 \(\frac{n^4}{\omega}\)
写代码的时候没有考虑清楚一些细节,比如写完才发现新图不会存在回路,所以有一些多余的特判
#include <bits/stdc++.h>
using namespace std;
const int N=205;
int Head[N],Next[N<<1],Adj[N<<1],tot=0;
char s[N];int to[N][N],U[N*N],V[N*N],cnt=0;
vector <int> v[N*N];
int fa[N];
int getrt(int x){
return fa[x]==x?x:fa[x]=getrt(fa[x]);
}
void addedge(int u,int v){
Next[++tot]=Head[u],Head[u]=tot,Adj[tot]=v;
Next[++tot]=Head[v],Head[v]=tot,Adj[tot]=u;
}
bool flag;
void merge(int x,int y){
if (getrt(x)==getrt(y)){
flag=false;
return;
}
fa[getrt(x)]=getrt(y);
addedge(x,y);
}
bool vis[N*N];
int n;
void dfs(int x){
vis[x]=true;merge(U[x],V[x]);
for (int i=0;i<v[x].size();i++)
if (!vis[v[x][i]]) dfs(v[x][i]);
}
void init(){
flag=true;
for (int i=1;i<=n;i++) fa[i]=i;
memset (Head,0,sizeof(Head));
for (int i=1;i<=tot;i++) Next[i]=Adj[i]=0;
tot=0;
}
char st[N][N][N];
int dis[N][N];
void getdist(int rt,int x,int f,int d){
dis[rt][x]=d;
for (int e=Head[x];e;e=Next[e])
if (Adj[e]!=f) getdist(rt,Adj[e],x,d+1);
}
bool check(){
if (!flag) return false;
if (tot!=n*2-2) return false;
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
dis[i][j]=-1;
for (int i=1;i<=n;i++) getdist(i,i,0,0);
for (int i=1;i<n;i++)
for (int j=i+1;j<=n;j++)
for (int k=1;k<=n;k++)
if ((dis[i][k]==dis[j][k])^(st[i][j][k]-'0')) return false;
return true;
}
void print(){
int num=0;
for (int i=1;i<=n;i++)
for (int e=Head[i];e;e=Next[e])
if (e&1) {
printf ("%d %d\n",i,Adj[e]);
num++;
}
assert(num==n-1);
}
int main(){
int T;scanf ("%d",&T);
while (T--){
scanf ("%d",&n);cnt=0;
for (int i=1;i<=n;i++)
for (int j=1;j<=i;j++)
to[i][j]=++cnt,U[cnt]=i,V[cnt]=j;
for (int i=1;i<=n;i++)
for (int j=i+1;j<=n;j++)
to[i][j]=to[j][i];
for (int i=1;i<=cnt;i++) vis[i]=false;
for (int i=1;i<=cnt;i++) v[i].clear();
for (int i=1;i<n;i++)
for (int j=i+1;j<=n;j++){
scanf ("%s",s+1);
for (int k=1;k<=n;k++){
if (s[k]=='1') {
v[to[i][k]].emplace_back(to[j][k]);
v[to[j][k]].emplace_back(to[i][k]);
}
st[i][j][k]=s[k];
}
}
for (int i=1;i<=cnt;i++)
if (!vis[i]) {
init();
dfs(i);
if (check()) {
puts("Yes");
print();
flag=true;
break;
}
flag=false;
}
if (!flag) puts("No");
}
return 0;
}