「杂题乱刷」P10678
upd:
哎哎哎,原来的题解没怎么写证明被叉了 /yun
所以我来补下证明。
upd2:
修改代码,增加代码可读性。
题目链接
解题思路
时间复杂度优于官解的做法。
首先我们观察到一个性质就是 \(\sum a_i = 2 \times (n - 1)\),因为一个树有 \(n - 1\) 条边。
注意到一棵树必定有叶子结点。
于是我们每次给树连边只需要拿一个剩余需连的边数为 \(1\) 的点和剩余需连的边数大于 \(1\) 的点相连即可,注意,如果没有剩余需连的边数大于 \(1\) 的点,需要特判,让另一个度数为 \(1\) 的点与这个点相连。
正确性显然。
下面是证明:
你注意到,你可以把一次加边操作看为将两个点的剩余度数减去 \(1\) 的操作,并且加边操作有至少一个点剩余的度数为 \(1\),因此,一个点连接的所有点的数量减去一的点的数量都是因为该点的度数为 \(1\),而另外一个点则是因为该点度数为 \(1\) 因此连接下一个点。
也就是说,最后形成的树可以抽象的看成菊花状,并且只要能连点,那么当前剩余需连度数为 \(1\) 的点都被尽量连到了同一个点上,并且树的直径大小一定是两个度数为 \(1\) 的点的路径长度,因此这样的连边方式也就保证了最后的树的直径最小。
时间复杂度 \(O(\sum n)\)。
参考代码
#include<bits/stdc++.h>
using namespace std;
#define forl(i,a,b) for(register long long i=a;i<=b;i++)
#define forr(i,a,b) for(register long long i=a;i>=b;i--)
#define forll(i,a,b,c) for(register long long i=a;i<=b;i+=c)
#define forrr(i,a,b,c) for(register long long i=a;i>=b;i-=c)
#define pb push_back
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
#define endl '\n'
#define QwQ return 0;
#define ll long long
ll t;
ll n;
queue<ll>q[200010];
ll a[200010],b[200010];
void solve()
{
cin>>n;
ll ma=0;
forl(i,1,n)
cin>>a[i],ma=max(ma,a[i]),b[a[i]]++,q[a[i]].push(i);
ll S=0;
while(S<n-1)
{
ll id1=q[1].front();
q[1].pop();
cout<<id1<<' ';
ll pd=0;
forl(j,2,ma)
{
if(!q[j].empty())
{
id1=q[j].front();
cout<<id1<<endl;
S++;
q[j].pop();
q[j-1].push(id1);
ll L=j-1;
while(L>=2)
{
id1=q[L].front();
cout<<id1<<' ';
cout<<q[1].front()<<endl;
S++;
q[1].pop();
q[L].pop();
q[--L].push(id1);
}
pd=1;
break;
}
}
if(!pd)
{
id1=q[1].front();
cout<<id1<<endl;
S++;
q[1].pop();
}
}
}
int main()
{
IOS;
t=1;
cin>>t;
while(t--)
solve();
QwQ;
}