<think>题目要求:给定一个长度为n的数列a,如果两个下标i,j满足a[i] & a[j] != 0(按位与不为0),则在这两个点之间有一条长度为a[i]+a[j]的无向边。求从点1(下标1)出发到所有点的最短路,如果不能到达则输出-1。
注意:点下标从1开始。
观察数据范围:n最大为10^5,a[i]在[0,2^30]之间。
如果直接枚举所有点对,判断是否有边并建图,那么边的数量可能达到O(n^2),显然不可行。因此需要优化建图。
思路:利用位运算的性质。两个数按位与不为0,说明存在至少一个二进制位k,使得两个数在该位上都为1。
因此,我们可以考虑对每个二进制位建立一个超级源点(或者理解为一种分组),对于每个数a[i],如果它在第k位为1,那么它就和代表第k位的超级源点相连。但是,这样会引入新的点,而且边的权值如何定义?
重新思考:我们想要避免O(n^2)的建边,那么可以考虑将每个数a[i]与它所有的二进制位代表的集合(也就是该数所有为1的位)连接。但是,这样建图后,两个数如果共享至少一个二进制位,那么它们就会通过该二进制位对应的超级节点相连。也就是说,原来两个数直接有边,现在变成了通过一个中间节点(超级节点)的两条边。
但是,这样边的权值发生了变化:原图中两个点i,j之间的边权是a[i]+a[j],而通过超级节点k连接时,边权变成了(a[i]+a[k]) + (a[k]+a[j]),其中a[k]并不存在,因为超级节点是我们引入的虚拟节点。
因此,我们需要重新设计边的权值。我们可以这样定义:
- 对于每个二进制位k(0到30),我们创建一个超级节点,编号为k+n(节点编号从1到n,所以超级节点从n+1开始)。
- 对于每个原始节点i(对应a[i]),如果a[i]的第k位为1,那么我们在节点i和超级节点k+n之间建立一条无向边,边权为a[i](注意,这里边权不是a[i]+a[k],因为超级节点没有值,我们只考虑原始节点的值,所以边权设为a[i])。这样,两个原始节点i和j如果都连接了同一个超级节点k,那么从i到j的路径就是i->k+n->j,总边权为a[i] + a[j]。
这样,原来i和j直接有边(边权为a[i]+a[j])的情况,现在变成了通过超级节点的一条路径,且总边权相同。因此,我们可以用这种建图方式代替原来的直接建边。
但是,这样建图后,图中可能包含一些原来没有的边:比如,两个数可能没有共同的二进制位,但是通过不同的超级节点路径相连。然而,题目要求是当且仅当两个数有公共二进制位(即按位与不为0)时才有边。而通过超级节点连接的两个原始节点,它们一定至少共享一个二进制位(因为都连接了同一个超级节点k,说明它们都有第k位为1)。所以,这样建图后,两个原始节点之间有路径当且仅当它们至少有一个公共二进制位,而且路径的边权和等于原来的直接边权(即a[i]+a[j])。另外,通过超级节点还可能形成更长的路径,但最短路算法会自动选择最短的路径,所以如果两个节点有公共二进制位,那么最短路就是a[i]+a[j](因为直接通过超级节点的路径权值就是a[i]+a[j],而如果通过多个超级节点,权值会更大,因为每次经过超级节点都会加上两个原始节点的值,而直接路径只加了一次两个原始节点的值?这里需要仔细分析)。
实际上,我们这样建图后,任意两个有公共二进制位的节点i和j,它们之间有一条路径:i->k+n->j,边权为a[i]+a[j]。同时,可能还有其他路径,比如i->k+n->l+n->j,但这样的路径边权为a[i] + a[j] + a[k](超级节点本身没有值,但边权是连接原始节点和超级节点的边权,这里a[k]并不是一个值,而是我们设定的边权为原始节点的值?不对,我们设定的边权是:从原始节点i到超级节点k+n的边权为a[i],从超级节点k+n到原始节点j的边权为a[j]。所以路径i->k+n->j的边权为a[i]+a[j]。而路径i->k+n->l+n->j的边权为a[i] + a[k](这里a[k]是超级节点k+n连向原始节点时,原始节点的值?不对,超级节点k+n连向原始节点j时,边权是a[j]?这里我们设定的边权都是原始节点的值,但是注意,同一个原始节点连接多个超级节点时,边权都是该原始节点的值。所以路径i->k+n->l+n->j的边权为:a[i](从i到k+n) + a[l](从k+n到l+n?不对,超级节点之间没有边!)注意,我们只建立了原始节点和超级节点之间的边,超级节点之间没有边。所以路径只能是原始节点->超级节点->原始节点。因此,两个原始节点之间只能通过一个超级节点相连,路径长度就是a[i]+a[j]。不可能通过两个超级节点,因为超级节点之间没有边。
但是,还有一种情况:同一个原始节点可以通过不同的超级节点连接到其他原始节点。例如,节点i连接了超级节点k和l,节点j连接了超级节点l和m,那么i和j可以通过超级节点l相连(路径i->l+n->j,边权a[i]+a[j]),也可以通过超级节点k和m?但这样就需要经过两个超级节点,而超级节点之间没有边,所以不行。因此,两个原始节点之间最多只有一条边(通过一个公共超级节点),且边权为a[i]+a[j]。所以,这样建图后,原始节点之间的直接边被拆成了两条边,但总边权不变,并且没有增加多余的边(因为超级节点之间没有边,所以路径长度不会减少?)。
然而,这里有一个问题:在原始图中,两个节点之间可能有多条边(如果它们有多个公共二进制位),但在我们建的图中,它们之间仍然只有一条路径(通过任何一个公共超级节点,路径长度都是a[i]+a[j]),所以实际上我们只保留了一条边。但最短路算法中,我们只需要知道两点之间的最小边权,而这里所有公共二进制位对应的路径边权都是a[i]+a[j],所以保留一条即可。
但是,这样建图后,图中的节点数变为n+31(最多31个超级节点,因为a[i]<=2^30,所以二进制位从0到30共31位),边数:每个原始节点最多连31条边(因为一个数的二进制最多31位为1),所以总边数最多31*n,即310万条边,在可接受范围内。
然后,我们在这个新图上运行最短路算法(Dijkstra),起点是节点1(原始节点1)。
注意:超级节点只是中间节点,我们最终要求的是原始节点1到其他原始节点的最短路。
另外,注意点1到点1的最短路是0。
但是,我们这样建图后,点1到点1的路径可能是什么?点1连接了一些超级节点,然后通过超级节点再回到点1?这样会形成一条边权为a[1]+a[1]=2*a[1]的路径。但显然,点1到点1的最短路应该是0。所以我们需要在算法开始时将起点1的距离设为0,然后算法运行中,0会是最小值,所以不会更新自己。
另外,原始图中,两个原始节点之间可能没有边(没有公共二进制位),那么在新图中,这两个原始节点之间就没有路径(因为必须通过公共超级节点),所以无法到达。
因此,算法步骤:
1. 创建图:节点总数n+31(或者32,为了保险,我们可以创建32个超级节点,编号从n+1到n+31,对应位0到30)。注意:位0对应最低位,位30对应最高位。
2. 对于每个原始节点i(1<=i<=n),对于每个二进制位k(0<=k<=30):
如果a[i]的第k位为1,则添加两条边(无向边):
从i到超级节点n+k+1(因为超级节点编号从n+1开始),边权为a[i]。
注意:无向边就是双向边,所以添加边(i, n+k+1, a[i])和(n+k+1, i, a[i])。
3. 初始化距离数组dis,长度为n+32(节点编号从1到n+31),初始化为一个很大的数(比如1e18)。将dis[1]设为0。
4. 使用优先队列(小根堆)的Dijkstra算法:
将节点1加入优先队列(距离为0)。
每次取出距离最小的节点u,然后遍历u的所有邻接边(u,v,weight):
如果dis[v] > dis[u] + weight,则更新dis[v] = dis[u] + weight,并将v加入优先队列。
5. 最后,对于每个原始节点i(1<=i<=n),输出dis[i];如果dis[i]还是初始值(即未被更新),则输出-1。
但是,这里有一个问题:在原始问题中,从点1到点i的最短路,在原始图中可能经过多个中间节点。而在新图中,我们通过超级节点来表示原始节点之间的直接边。那么,新图中点1到点i的最短路是否对应原始图中的最短路?
考虑原始图中的一条路径:1->j->i。在原始图中,这条路径的权值为(a[1]+a[j]) + (a[j]+a[i]) = a[1] + 2*a[j] + a[i]。
在新图中,路径可能是:1->超级节点k->j->超级节点l->i。边权为:a[1](1到超级节点k) + a[j](超级节点k到j) + a[j](j到超级节点l) + a[i](超级节点l到i) = a[1] + a[j] + a[j] + a[i] = a[1] + 2*a[j] + a[i],与原始图相同。
因此,新图完全模拟了原始图的边权。而且,新图中原始节点之间没有直接边,只能通过超级节点连接。所以,新图可以正确模拟原始图。
但是,我们还需要注意:原始图中,同一个原始节点出发再回到同一个原始节点是不允许的(除非有自环,但题目中自环的条件是a[i]&a[i]!=0,即a[i]!=0,所以当a[i]!=0时,自环存在,边权为2*a[i])。在新图中,自环表现为:i->超级节点k->i,边权为a[i]+a[i]=2*a[i]。所以自环也被正确表示。
因此,我们可以用上述方法建图,然后跑Dijkstra。
但是,我们还需要考虑a[i]为0的情况。如果a[i]为0,那么它和任何数按位与都是0,所以它没有边连向其他节点(包括自己,因为0&0=0,所以也没有自环)。因此,如果a[i]=0,那么它在新图中不会连接任何超级节点(因为它的每一位都是0)。所以,这样的点除了起点(如果是起点1,则距离为0)外,其他点都无法到达(除非起点本身,但起点1的a[1]如果是0,那么它也没有边连出去,所以除了自己其他点都无法到达?但题目要求点1到点1的最短路是0,所以起点1不管a[1]是多少,我们都要设置dis[1]=0)。
另外,注意:如果起点1的a[1]为0,那么它没有边连出去,所以它只能到达自己。
因此,算法需要处理a[i]=0的情况:这样的点不会连向超级节点,所以在新图中是孤立的(除了起点1)。
最后,注意:超级节点之间没有边,所以不会增加额外的路径。
实现细节:
- 二进制位枚举:从0到30,共31位。
- 超级节点编号:我们设超级节点编号为n+1+k(k从0到30),这样一共有31个超级节点,编号范围是n+1到n+31。
- 建图:使用邻接表,每个节点(包括超级节点)存储邻接表(节点和边权)。
- 注意:节点总数是n+31,数组大小要开够。
时间复杂度:O((n+31) * log(n+31)),因为边数是31*n(约310万),节点数n+31(约10万),所以Dijkstra的时间复杂度是O((n+m)log(n)),这里m=310万,n=10万,log(n)约为17,所以总操作次数约为(10万+310万)*17≈5400万,在C++中可能勉强通过(1秒),但题目时间限制1秒,需要优化常数。
另一种思路:不显式建图,而是利用位运算的性质进行优化。但是,本题已经优化了建图(边数从O(n^2)降到O(n)),所以应该可以接受。
但是,我们也可以尝试不建超级节点,而是用另一种思路:点-超级节点的结构已经将边数降到了O(n*位数),所以可以接受。
另外,在Dijkstra算法中,我们可能会更新到超级节点,然后从超级节点再更新其他原始节点。这样,每个超级节点可能会被多次访问,但由于优先队列的性质,每个节点只会被扩展一次(如果使用标记数组,确保每个节点只从优先队列中取出一次)。
因此,代码步骤:
1. 读入n和数组a(大小为n+1,a[1]到a[n])。
2. 创建邻接表:大小为n+32(节点编号1到n+31)。
3. 对于每个原始节点i(1~n):
如果a[i]为0,则跳过(不连边)。
否则,枚举二进制位k(0~30):
如果a[i]的第k位为1:
添加边:i -> (n+1+k),权值为a[i]
添加边:n+1+k -> i,权值为a[i] (无向边,所以双向)
4. Dijkstra:
dis数组,大小为n+32,初始化为一个很大的数(比如1e18,因为最坏情况,边权最大2^30,路径最多10^5条边,总权值最大10^5*2^31,约2e15,所以1e18够用)。
dis[1]=0。
优先队列(小根堆):存储(d, u),表示节点u的当前距离为d。
从节点1开始,更新所有邻居。
5. 最后,对于每个原始节点i(1<=i<=n):
如果dis[i] >= INF(一个很大的数),则输出-1,否则输出dis[i]。
注意:超级节点的距离我们不需要输出。
但是,我们更新超级节点是为了通过它们更新其他原始节点。
另外,注意:同一个原始节点可能被多次更新,但优先队列会处理。
但是,这里有一个问题:从原始节点到超级节点的边权为a[i],而从超级节点到原始节点的边权也是a[i](同一个原始节点i)。这样,当我们从原始节点i到达超级节点k时,距离为dis[i]+a[i],然后从超级节点k到达原始节点j时,距离为dis[i]+a[i]+a[j]。而原始节点i和j之间通过超级节点k的路径总权值为a[i]+a[j],所以从i到j的路径权值就是dis[i] + a[i] + a[j]。但是,在Dijkstra中,我们是从起点1开始扩展的,所以这个dis[i]是起点1到i的距离。
因此,整个路径是:起点1->...->i->超级节点k->j。所以,从起点1到j的距离等于起点1到i的距离加上a[i]+a[j]。
但是,我们也可以从起点1直接通过超级节点到达j:如果起点1和j有公共二进制位,那么路径1->超级节点->j,权值为a[1]+a[j]。所以,起点1到j的距离可能由多条路径更新。
因此,算法会自动选择最小的路径。
下面用样例验证:
输入:5
1 5 6 7 8
点1:a[1]=1
点2:a[2]=5 -> 二进制101
点3:a[3]=6 -> 二进制110
点4:a[4]=7 -> 二进制111
点5:a[5]=8 -> 二进制1000
根据题目,点1到其他点的最短路:
1->1: 0
1->2: 1+5=6(因为1和5的二进制:1(001)和5(101)有公共最低位,所以有边)
1->3: 1和6(001和110)没有公共位?不对,1的二进制是001,6的二进制是110,按位与为0,所以没有直接边。
但是,点1可以通过点2到达点3:1->2->3:1+5 + 5+6 = 17
1->4: 1和7(001和111)有公共最低位,所以有边:1+7=8
1->5: 1和8(001和1000)没有公共位,而且点5和其他点(除了点4?):
点5(8)和点4(7):8(1000)和7(0111)按位与为0,所以没有边;点5和点3:8(1000)和6(0110)按位与0;点5和点2:8和5(1000和0101)按位与0。所以点5不可达。
输出:0 6 17 8 -1
建图:
点1:1 -> 二进制:第0位为1(最低位),所以连接超级节点n+1(即6,因为n=5,所以超级节点编号6,7,...,36,但只需要0~30位,所以超级节点编号6到36?不对,我们只需要31个超级节点,编号6到36?实际上,我们只需要0~30位,所以超级节点编号为5+1+k(k从0到30),即6到36。但实际我们只需要5个节点?不对,k从0到30,所以超级节点编号为6到36(31个节点)。
具体:
点1(1): 连接超级节点6(因为k=0,第0位为1)
点2(5): 二进制101,第0位和第2位为1,所以连接超级节点6(k=0)和8(k=2)
点3(6): 二进制110,第1位和第2位为1,所以连接超级节点7(k=1)和8(k=2)
点4(7): 二进制111,第0,1,2位为1,所以连接超级节点6,7,8
点5(8): 二进制1000,第3位为1,所以连接超级节点9(k=3)
然后,运行Dijkstra:
dis[1]=0
从点1出发,更新所有邻居:超级节点6,距离为0+a[1]=1。
然后,从超级节点6出发,更新所有邻居:点1(但已经访问过,跳过),点2(距离1+a[2]=1+5=6),点4(距离1+a[4]=1+7=8)。
所以,dis[2]=6, dis[4]=8。
然后,从点2出发(距离6),更新它的邻居:超级节点6(已经访问),超级节点8(距离6+a[2]=6+5=11)。
然后,从点4出发(距离8),更新它的邻居:超级节点6(已访问),超级节点7(距离8+a[4]=8+7=15),超级节点8(距离8+a[4]=8+7=15)。但是超级节点8已经被点2更新为11?所以这里会更新超级节点7和8(但超级节点8之前是11,现在从点4更新为15,不更新)。
然后,从超级节点8(距离11)出发,更新它的邻居:点2(已访问),点3(距离11+a[3]=11+6=17),点4(距离11+a[4]=11+7=18,但点4已经是8,所以不更新)。
然后,从点3出发(距离17),更新邻居:超级节点7(距离17+6=23),超级节点8(距离17+6=23)。然后更新超级节点7:从超级节点7更新点4(距离23+7=30,但点4已经是8,所以不更新)和点3(已访问)。
另外,超级节点7(距离15)在之前被点4更新过,然后从超级节点7出发,更新点3(距离15+6=21)和点4(15+7=22,但点4已经是8,不更新)。但是,点3在超级节点8更新时已经得到17,所以21>17,不会更新。
所以,最终:
dis[1]=0
dis[2]=6
dis[3]=17
dis[4]=8
dis[5]:没有被更新,所以为INF,输出-1。
符合样例输出。
注意:在Dijkstra中,我们可能会先更新超级节点,然后再由超级节点更新其他原始节点。所以,超级节点也是图中的节点,需要参与更新。
但是,我们不需要输出超级节点的距离。
另外,注意:同一个超级节点可能被多个原始节点更新,所以可能会多次进入优先队列,但我们用dis数组记录最小值,当从优先队列中取出一个节点时,如果它的距离已经比dis数组中的值大,就可以跳过。
代码实现:
使用vector邻接表,注意节点数最大为n+31=100000+31=100031。
边数:每个原始节点最多连31条边,所以总边数不超过31*2*n(双向边,所以乘2?不对,每条边存两次:对于原始节点i,添加一条从i到超级节点的边,同时也会在超级节点的邻接表中添加一条从超级节点到i的边。所以总边数约为2*31*n,即620万条边。在C++中,使用vector邻接表,空间大约为620万*2(因为每个边存两次)?不对,在邻接表中,每条边在起点和终点各存一次?不对,无向边,我们在添加时:
adj[i].push_back({super, a[i]});
adj[super].push_back({i, a[i]});
所以,每条无向边被拆成两条有向边,所以总边数(有向边)为2*(31*n) = 62*n,即62*100000=620万条边。在C++中,vector邻接表可以存储,但要注意空间(大约620万条边,每条边是一个pair<int, long long>,大约16字节,总空间约100MB,题目内存512MB,足够)。
然后,优先队列中最多有100031个节点,但每次扩展会加入多个节点,但每个节点最多被扩展一次(标记后不再扩展),所以总操作次数为边数(即620万次更新,优先队列操作次数为O(m log n),log(100031)约17,所以总操作次数约1亿,在C++中可能勉强1秒(在优化常数的情况下)。
注意:使用优先队列(堆)的Dijkstra,每个节点只扩展一次(用vis数组标记),但更新距离时,同一个节点可能被多次加入优先队列(但只有第一次取出时扩展),所以优先队列中最多有O(m)个元素。
代码框架:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100000 + 31; // 节点数:n+31
const ll INF = 1e18;
int n;
ll a[100000+10]; // 原始数组,1-indexed
vector<pair<int, ll>> adj[MAXN+10]; // 邻接表,节点编号1到n+31
ll dis[MAXN+10];
bool vis[MAXN+10]; // 标记是否已确定最短路
void dijkstra(int start) {
for (int i = 1; i <= n+31; i++) { // 初始化,节点1到n+31
dis[i] = INF;
vis[i] = false;
}
dis[start] = 0;
priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<pair<ll, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
ll d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto &edge : adj[u]) {
int v = edge.first;
ll w = edge.second;
if (dis[v] > d + w) {
dis[v] = d + w;
pq.push({dis[v], v});
}
}
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
}
// 初始化邻接表
// 注意:节点编号:原始节点1~n,超级节点n+1~n+31(对应位0~30)
// 清空邻接表,但MAXN是100031,所以从1到n+31
for (int i = 1; i <= n+31; i++) {
adj[i].clear();
}
// 对于每个原始节点i,枚举二进制位
for (int i = 1; i <= n; i++) {
if (a[i] == 0) {
// 0没有边,跳过
continue;
}
for (int k = 0; k <= 30; k++) {
// 检查第k位是否为1
if (a[i] & (1LL << k)) {
int super = n + 1 + k; // 超级节点编号
// 添加无向边:i到super,边权为a[i]
adj[i].push_back({super, a[i]});
adj[super].push_back({i, a[i]});
}
}
}
dijkstra(1);
// 输出结果:对于每个原始节点i(1~n)
for (int i = 1; i <= n; i++) {
if (dis[i] == INF) {
printf("-1");
} else {
printf("%lld", dis[i]);
}
if (i < n) printf(" ");
else printf("\n");
}
return 0;
}
注意:超级节点编号:k从0到30,超级节点编号为n+1+k。所以当n=5时,超级节点编号为6,7,...,36(共31个)。但我们的节点总数是n+31=36,所以数组要开够。
但是,在邻接表中,我们给节点编号1~n+31(即1~36)都分配了空间,所以没有问题。
另外,注意:1<<k 对于k=30,可能会溢出int,所以用1LL<<k。
样例测试:n=5,a[1]=1, a[2]=5, a[3]=6, a[4]=7, a[5]=8。
运行后,dis[1]=0, dis[2]=6, dis[3]=17, dis[4]=8, dis[5]=INF(-1)。
符合。
但是,还有一种情况:起点1的a[1]为0,那么它不会连接任何超级节点,所以只能到达自己。
测试:n=1, a[1]=0,则输出0。
另外,注意:如果两个原始节点之间没有公共二进制位,则无法到达,输出-1。
再测试:n=2, a[1]=1, a[2]=2。1和2:二进制01和10,按位与为0,所以没有边。所以点1到点2不可达,输出0 -1?不对,点1到点1是0,点1到点2不可达。
建图:点1(1)连接超级节点(第0位)n+1(即2+1=3?n=2,超级节点编号3)。
点2(2)连接超级节点(第1位)n+1+1=4。
所以点1和点2没有公共超级节点,所以点2不会被更新。
输出:0 -1。
正确。
但是,还有一种路径:点1->超级节点3->点1->超级节点3->...,但这样不会更新点2。
所以,算法正确。
最后,注意:a[i]可能为0,所以跳过建边。但是,起点1的a[1]可能为0,我们仍然设置dis[1]=0,然后它没有边连出去,所以其他点都是INF。
因此,代码完整。
但是,在Dijkstra中,我们使用了vis数组,确保每个节点只被扩展一次,这是正确的。
另外,优先队列中可能有很多冗余节点,但用vis数组跳过,可以避免重复扩展。
现在,提交代码时,注意输入输出效率,使用scanf和printf。
由于n最大10^5,边数620万,Dijkstra算法时间复杂度O(m log n),大约1e6*log(1e5)≈1e6*17=1.7e7次操作,在C++中应该可以1秒内完成。
但是,常数较大(优先队列操作),所以注意使用较快的IO。
另外,注意:超级节点编号从n+1到n+31,所以节点总数是n+31,不要开小数组。
最后,输出格式:第i个为1到i的最短路长度,空格分隔,行末无空格。
代码:
注意:在添加边时,如果a[i]为0,则跳过。另外,超级节点编号:n+1+k(k从0到30)。
我们使用0-indexed的k(0~30)代表31个二进制位。
提交代码,注意内存:邻接表大约620万条边,每个pair<int,ll>是12字节(int4,ll8),总空间约6200000*12=74.4MB,加上其他数组,应该不会超过512MB。
运行时间:优先队列操作约620万次push和pop,每次O(log(100031))≈17次操作,总操作次数约1亿,在C++中1秒可能有点紧,但应该能过(在优化编译下)。
如果超时,可以尝试使用vector邻接表时,预先保留空间。
但本题n最大10万,边数620万,vector的push_back可能会引起多次重新分配,所以可以预先reserve:
for (int i = 1; i <= n+31; i++) {
adj[i].reserve(50); // 原始节点最多31条边,超级节点最多有多少边?一个超级节点可能被很多原始节点连接,但无法预知,所以不reserve也可以。
}
或者,在添加边时,每个原始节点最多31条边,但超级节点的边数可能很多(如果很多数都有某一位,那么该超级节点的度就很大)。但总边数固定,所以vector的push_back总时间O(m),可以接受。
下面写代码。
注意:在Dijkstra中,优先队列用greater,所以pair的第一个元素是距离,第二个是节点。
另外,距离用long long。
最后,输出时,每个原始节点的距离,如果是INF则输出-1。
代码:</think>### 问题分析
题目要求计算从节点1到所有节点的最短路径,其中节点i和j之间存在边的条件是a[i] & a[j] ≠ 0(按位与不为零),边权为a[i] + a[j]。由于直接建图会导致边数达到O(n²),需优化建图。
#### 优化思路
利用二进制位性质:若两个数在某一位均为1,则它们有边。为每个二进制位k(0≤k≤30)创建超级节点(编号n+1+k)。对于每个节点i:
- 若a[i]的第k位为1,则在i和超级节点n+1+k间添加无向边,边权为a[i]。
这样,若两个节点有公共位k,则路径i→(n+1+k)→j的边权和为a[i] + a[j],与直接边等价。
#### 算法步骤
1. **建图**:
- 原始节点:1~n
- 超级节点:n+1 ~ n+31(对应31个二进制位)
- 对每个非零a[i],枚举其二进制位k,添加边(i, n+1+k, a[i])和反向边。
2. **Dijkstra求最短路**:
- 初始化距离数组dis,起点dis[1]=0。
- 使用优先队列(小根堆)按距离升序处理节点。
3. **输出**:对每个原始节点i,输出dis[i](若未更新则输出-1)。
#### 复杂度
- 节点数:n + 31
- 边数:≤ 31 × 2 × n = 62n(约620万条)
- 时间复杂度:O((n + m) log n),其中m为边数,可接受。
### 代码实现
```cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100000 + 31;
const ll INF = 1e18;
int n;
ll a[100010];
vector<pair<int, ll>> adj[MAXN];
ll dis[MAXN];
bool vis[MAXN];
void dijkstra(int start) {
for (int i = 1; i <= n + 31; i++) {
dis[i] = INF;
vis[i] = false;
}
dis[start] = 0;
priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<pair<ll, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
ll d = pq.top().first;
int u = pq.top().second;
pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto &edge : adj[u]) {
int v = edge.first;
ll w = edge.second;
if (dis[v] > d + w) {
dis[v] = d + w;
pq.push({dis[v], v});
}
}
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
}
for (int i = 1; i <= n + 31; i++) {
adj[i].clear();
}
for (int i = 1; i <= n; i++) {
if (a[i] == 0) continue;
for (int k = 0; k <= 30; k++) {
if (a[i] & (1LL << k)) {
int super = n + 1 + k;
adj[i].push_back({super, a[i]});
adj[super].push_back({i, a[i]});
}
}
}
dijkstra(1);
for (int i = 1; i <= n; i++) {
if (dis[i] == INF) printf("-1");
else printf("%lld", dis[i]);
if (i < n) printf(" ");
else printf("\n");
}
return 0;
}
```
### 示例验证
**输入**:
```
5
1 5 6 7 8
```
**输出**:`0 6 17 8 -1`
- 节点1:距离0
- 节点2:路径1→(位0)→2,边权1+5=6
- 节点3:路径1→2→3,边权(1+5)+(5+6)=17
- 节点4:路径1→(位0)→4,边权1+7=8
- 节点5:无公共位,不可达