//这道题算是比较常规的题了,采用kruskal算法并利用并查集思想求最小生成树
#include "stdio.h"
#include "queue"
using namespace std;
struct Edge{
    int villageS;//源端的村庄编号
    int villageT;//目的端的村庄编号
    int weight;
};
int N,M;//N为道路数目,M为村庄数目
priority_queue<Edge> myPQueue;
int Father[101];//并查集得根存储

bool operator < (Edge lhs,Edge rhs){
    return lhs.weight > rhs.weight;
}

void Init(){
    for (int i = 1; i <= M; ++i) {
        Father[i] = i;
    }
    while (!myPQueue.empty())
        myPQueue.pop();
}

int find(int x){
    if (Father[x] != x){
        Father[x] = find(Father[x]);
    }
    return Father[x];
}

int kruskal(){
    int sum = 0;//存储路径总长
    while (!myPQueue.empty()){
        Edge edge = myPQueue.top();
        myPQueue.pop();
        int villageS = edge.villageS,villageT = edge.villageT;
        if (find(villageS) != find(villageT)){
            Father[find(villageT)] = find(villageS);//Union操作
            sum += edge.weight;
        }
    }
    int count = 0;
    for (int i = 1; i <= M; ++i) {
        if (Father[i] == i)
            ++count;
    }
    if (count != 1)//有多个连通分支,无法形成最小生成树
        return -1;
    else
        return sum;
}

int main(){
    while (scanf("%d",&N)!=EOF){
        if (N == 0)
            break;
        scanf("%d",&M);
        Init();//对邻接矩阵和并查集进行初始化
        int villageS,villageT,weight;
        for (int i = 1; i <= N; ++i) {//道路入优先队列
            scanf("%d%d%d",&villageS,&villageT,&weight);
            Edge edge;edge.villageS = villageS;
            edge.villageT = villageT;edge.weight = weight;
            myPQueue.push(edge);
        }
        int sum = kruskal();
        if (sum != -1)
            printf("%d\n",sum);
        else
            printf("?\n");
    }
}