#include<bits/stdc++.h>
using namespace std;
using ll = long long;
/*
算法思想:
这是一个带「必选边」的最小生成树问题,基于Kruskal算法改进
核心逻辑:
1. 题目要求必须包含所有 p=1 的必选边,哪怕它们形成环
2. 在此基础上,用 p=0 的可选边(按权值从小到大)补全生成树,保证总代价最小
3. 最终需要确保所有城市(顶点)都连通,否则输出 -1
适用场景:稀疏图(V与E数量接近)
*/
// 定义边的结构体:存储每条边的原始编号、端点、权值、必选标记、选中标记
struct Edge {
ll i; // 边的原始输入编号(输出时需要用到)
ll u, v; // 边的两个端点(城市编号)
ll w; // 修建这条道路的花费(权值)
ll p; // 必选标记:1表示必选,0表示可选
bool r = false; // 标记这条边是否被选中加入最终方案
};
// 全局变量定义
ll n, m; // n:城市数量(顶点数),m:修路计划数量(边数)
ll cnt = 0; // 最终选中的边的总数
const ll MAXN = 1e5 + 5; // 最大顶点数,根据题目规模调整
ll parent[MAXN]; // 并查集父节点数组:维护顶点的连通性
ll rank_[MAXN]; // 并查集秩数组:用于按秩合并,优化并查集效率
// 并查集初始化:每个顶点初始时自成一个连通分量
// 算法前置步骤:为连通性检查准备基础结构
void init(ll n) {
for (ll i = 1; i <= n; ++i) { // 城市编号通常从1开始
parent[i] = i; // 每个顶点的父节点初始化为自己
rank_[i] = 0; // 初始秩为0,表示树的高度为0
}
}
// 并查集查找操作(带路径压缩)
// 功能:快速找到顶点x所在连通分量的根节点,路径压缩可将后续查询复杂度降为近似O(1)
ll find(ll x) {
if (parent[x] != x) {
// 路径压缩:将x直接指向根节点,减少后续查找的层数
parent[x] = find(parent[x]);
}
return parent[x];
}
// 并查集合并操作(按秩合并)
// 功能:将两个不同的连通分量合并,按秩合并保持树的平衡,避免退化成链表
void union_(ll x, ll y) {
ll root_x = find(x); // 找到x的根节点
ll root_y = find(y); // 找到y的根节点
if (root_x == root_y) return; // 已在同一集合,无需合并
// 秩小的树合并到秩大的树下,保持树的平衡
if (rank_[root_x] < rank_[root_y]) {
parent[root_x] = root_y;
} else {
parent[root_y] = root_x;
// 若秩相等,合并后根节点的秩+1(树的高度增加1)
if (rank_[root_x] == rank_[root_y]) {
rank_[root_x]++;
}
}
}
// Kruskal算法主函数:求解带必选边的最小生成树
void kruskal(ll n, vector<Edge>& edges) {
// --------------------------
// 算法步骤1:对所有边排序
// 排序规则:
// 1. 优先处理必选边(p=1 排在 p=0 前面)
// 2. 同优先级下,按权值w升序(保证可选边选代价最小的,必选边也优先选代价小的)
// --------------------------
sort(edges.begin(), edges.end(), [](Edge a, Edge b) {
if (a.p != b.p) return a.p > b.p; // 第一关键字:p降序
else return a.w < b.w; // 第二关键字:w升序
});
// --------------------------
// 算法步骤2:初始化并查集
// --------------------------
init(n);
// 哈希表:记录每个顶点是否被覆盖(用于最终检查所有城市是否连通)
unordered_set<ll> s;
// --------------------------
// 算法步骤3:遍历排序后的边,贪心选择符合条件的边
// --------------------------
for (ll i = 0; i < m; i++) {
ll u = edges[i].u, v = edges[i].v, w = edges[i].w, p = edges[i].p;
// 情况1:当前边的两个端点不在同一连通分量
// → 选这条边不会形成环,合并连通分量并标记为选中
if (find(u) != find(v)) {
union_(u, v); // 合并两个连通分量
s.insert(u);
s.insert(v);
cnt++; // 选中边数+1
edges[i].r = true; // 标记该边被选中
}
// 情况2:当前边是必选边(p=1),即使两个端点已连通(形成环)也必须选中
// → 满足题目“必选计划必须包含”的要求
else if (p == 1) {
edges[i].r = true; // 标记该边被选中
cnt++; // 选中边数+1
}
}
// --------------------------
// 算法步骤4:检查所有城市是否都连通
// --------------------------
for (ll i = 1; i <= n; i++) {
if (s.find(i) == s.end()) { // 存在城市未被任何边覆盖 → 无法连通
cout << -1;
return;
}
}
// --------------------------
// 算法步骤5:输出结果
// --------------------------
cout << cnt << "\n";
for (ll i = 0; i < m; i++) {
if (edges[i].r == true) {
cout << edges[i].i << " "; // 输出选中边的原始编号
}
}
}
int main() {
// 输入处理:读取城市数和计划数,然后读取每条边的信息
cin >> n >> m;
vector<Edge> edges(m);
for (ll i = 0; i < m; i++) {
edges[i].i = i + 1; // 边的编号从1开始(和输入顺序一致)
cin >> edges[i].u >> edges[i].v >> edges[i].w >> edges[i].p;
}
// 执行带必选边的Kruskal算法
kruskal(n, edges);
return 0;
}