Fork me on GitHub

HDU1233-还是畅通工程

继续刷邝斌带你飞最小生成树专题

HDU1233

浙大的复试题 —— 浙大计算机研究生复试上机考试好几年都有这题的变形

这玩意不应该是PAT上的吗?哦也对,PAT不配叫OJ了,浙大陈越姥姥搞的像商业化的东西

 

普里姆写了,克鲁斯卡尔也挺简单的,写一个普里姆堆优化的优先队列吧

普里姆堆优化要用vector存图好处是遍历的时候能精准找出所有以某点开都的点,不用(像邻接矩阵一样)遍历所有点。至于链式前向星有点大材小用了

 

写堆优化的时候,发现之前博客的堆优化写法,太模板了,傻逼,根本就不灵活,所有无意义的东西写进去都叫照抄和没彻底理解,比如我刚发现,真正堆优化代码不需要lowcost数组,之前那个题西路糊涂就AC了,根本没理解堆优化普里姆本质,去掉依旧可以AC

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<iostream>
 4 #define MAX 27//点数
 5 #define maxn 76//边数
 6 #include<vector>
 7 #include<queue>
 8 using namespace std;
 9 //int lowcost[MAX];
10 int is_in[MAX];//是否已经染色
11 int cnt;
12 struct nod
13 {
14     int from, to, dis, ju;
15     bool operator<(const nod x) const{
16         return dis > x.dis;
17     }
18 };
19 vector<nod> w[maxn];
20 int main()
21 {
22     int n;
23 
24     char lab_from;//记录每行首个英文字母
25     int lab_from_int;//转为int存
26 
27     int k;//首个字母通往的点,有k个
28 
29     char lab_to;//记录每行首个之后的字母
30     int lab_to_int;//转为int存
31 
32     int dis;//记录每行首个字母跟之后字母的距离
33 
34     nod now;
35     while(cin>>n&&n){
36 //        w.clear();
37         for (int i = 0; i < maxn; ++i) {
38             w[i].clear();
39         }
40         for(int i=0;i<n-1;i++){//比如样例,总共9个字母,接下来只需要输入8行
41             cin>>lab_from;
42             cin>>k;
43             lab_from_int=lab_from-65;//A是65
44             for(int j=0;j<k;j++){
45                 cin>>lab_to;
46                 cin>>dis;
47                 lab_to_int=lab_to-65;
48 
49                 now.from = lab_from_int,
50                 now.to = lab_to_int,
51                 now.dis = dis,
52                 now.ju = 1;
53                 w[lab_from_int].push_back(now);
54 
55                 now.from = lab_to_int,
56                 now.to = lab_from_int,
57                 now.dis=dis,
58                 now.ju = 0;
59                 w[lab_to_int].push_back(now);
60             }
61         }
62         int sum=0;
63 //        memset(lowcost,0x3f,sizeof(lowcost));
64         memset(is_in,0,sizeof(is_in));
65         priority_queue<nod> pq;
66         nod ne;
67 //        lowcost[0] = 0;
68         is_in[0]=1;
69         for (int i = 0; i < w[0].size(); ++i)
70             pq.push(w[0][i]);
71         while (!pq.empty()){
72             now = pq.top();
73             pq.pop();
74             if(is_in[now.from]==1 && is_in[now.to]==1)
75                  continue;
76             is_in[now.to]=1;
77             sum+=now.dis;
78             for (int i = 0; i < w[now.to].size(); ++i){
79                 ne = w[now.to][i];
80                 if( (is_in[ne.to]==0)){// && (lowcost[ne.to] > ne.dis) ){
81 //                    lowcost[ne.to] = ne.dis;
82                     pq.push(ne);
83                 }
84             }
85         }
86         cout<<sum<<endl;
87     }
88 }
View Code

感觉找结构体的dis成员变量的最小好蠢啊,我就觉得挺鸡巴der的,lowcost在堆优化里没啥用,去掉,然后我去看我去掉的这个写法,就上面这个View Code,

具体原理是:

AB距离10,

AC距离40,

BC距离20,

一开始AB跟AC都压入了优先队列,AB最先弹出来,理由是vector这个存图的方式,A点伸出去的点,AB最短

然后BC加进来了,这总共是30了,然后剩下AC的40了,弹出,但发现from和to都is_in过,就没操作,然后队列空,就结束了,可是当点多起来,这些不必要的弹出就很der儿啊,

所以仔细想下,我的优先队列里,本质是存的边,即成员变量,上面的例子也看到了,存了仨边,最后AC不需要是因为AC都is_in了,那我能否存点,A默认进来后,我就把lowdis当作优先队列里重载判断的东西,

因为我发现我的那个写法只是利用优先队列选出了最小值,却没更新。那我如果更新了,lowdis[2],是不是就不需要再判断40那个了,即存的是点。之前博客搜“优先队列方法必须在弹出最小值后再加个判断”也提到这个问题了,只是论证了必须判断一下,但没往下思考,其实优先队列那也是可以优化的。lowdis[B]作为10弹出后,更新,lowdis[C]作为20弹出,然后就没了,这是存点比存边的好处。

也很像迪杰斯特拉的堆优化思想,(其实是SPFA,见“放屁”那段)

试着写下。

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<iostream>
 4 #include<vector>
 5 #include<queue>
 6 using namespace std;
 7 #define MAX 100
 8 struct Edge{
 9     int from;
10     int to;
11     int dis;
12     int lowdis;
13 }edge;
14 
15 
16 bool operator<(Edge a, Edge b){
17     return a.lowdis>b.lowdis;
18 }
19 
20 vector<Edge>G[MAX];
21 int N;
22 
23 int map[MAX][MAX];
24 int main()
25 {
26      while(scanf("%d",&N)!=EOF && N){
27         memset(map, 0x3f, sizeof(map));
28         memset(G,0,sizeof(G));
29 //        如果是vector<Node>G用G.clear(),就是单纯的清空
30 //        如果是vector<Node>G[MAX]用memset 0,我估计应该是size为0
31 //        我也不知道为啥
32         for(int i=0; i<N*(N-1)/2; i++){
33             int a,b,c;
34 //            cin>>a>>b>>c;
35             scanf("%d%d%d",&a,&b,&c);
36             edge.from=a;
37             edge.to=b;
38             edge.dis=c;
39             G[a].push_back(edge);
40 
41             map[a][b]=c;//下面可以快速获取ab两点距离,vector没法做到
42         }
43         priority_queue<Edge>q;
44         while(!q.empty())
45             q.pop();
46         for(int i=0; i<G[1].size(); i++){
47 //            q.push(G[1][i]);
48             edge.to=G[1][i].to;
49             edge.lowdis=G[1][i].dis;
50 
51             q.push(edge);
52         }
53         while(!q.empty()){
54             edge=q.top();
55             q.pop();
56 
57 //            for(int i=0; i<G[edge.to].size(); i++){
58 //                if(map[G[edge.to][i].from] [ G[edge.to][i] ] >
59 //                   map[G[edge.to]          [ G[edge.to][i] ]        //C++基本语法都给我干懵逼了
60 //                   ){//妈逼的再次验证不应该看他们的代码,被这个lowdis绕进去了,不舍得丢弃,导致一直没写出来,其实lowdis也不用
61 //
62 //                }
63 //            }
64 
65             for(int i=0; i<G[edge.to].size(); i++){
66                 if(map[ G[edge.to][i].from ][ G[edge.to][i] ] >
67                    map[ edge.to ][ G[edge.to][i] ]){
68 
69                 }
70             }
71         }
72     }
73 }
View Code

写到一半怎么都写不下去了,因为需要压入队列的值,即上面例子的AC原本是40,因为B的弹出,给B染色,导致到C最近的点不是A了,而是B了,即BC的那个20,可是回顾迪杰斯特拉是选出就立马加入,不涉及修改队列中权值的事,还是C++的vector、结构体、队列、或许还有个map这些东西不咋会用

补充思考:

(刚发现全网都说像迪杰斯特拉,放屁,人云亦云的家伙,根本不是迪杰斯特拉,是贝尔曼或者SPFA,因为他其实有点像处理负环的算法,即顶点到某点X的距离dis,对于迪杰斯特拉来说,dis就是顶点到X的最短距离,但其实有可能有某点P的加入,P为负数,使得顶点到达X的距离小于dis,

但上面ABC那个例子确实是按照迪杰斯特拉来的,即走的是A→B→C,成为最短路。

但注意,最小生成树的思想,起手写法是:与A连接的点里,dis最小的,作为顶点到它的,最终的,最小生成树的路线中的一条。

有这个起手操作,那下面想堆优化,或者说队列优化,就涉及到了更新,当然像最近的一个“view”里的那种也行,就是太der了感觉,即:选了最小的点权值后,没沾亲带故的更新,而是把该更新的,更小的权值,也加入到队列里,即更新前后的权值都在队列里,然后,取短的先弹出了,然后把路“封死”,即点染色,那等长的再弹出的时候,就不会有任何操作了,感觉搞JB蠢

那就想到去套壳,想想之前的最短路贝尔曼算法是处理负环的,即不断更新距离,那贝尔曼的队列优化写法——SPFA正好可以参考。妈蛋,最短路全忘记了,回顾贝尔曼算法,然后回顾SPFA(妈的这是SPAF判断负环,不涉及更新貌似,换一个博客回顾),哎真他么服了我操,回顾个SPFA用这么久

换了个也不是处理负权的,妈的我发现最短路SPFA写过涉及负权的都tm是负环正环的事艹,好像没有真正处理负环更新的那种。

我就是想修改队列里的元素,查了查queue没有这功能,又查了个博客见###5,重新学了下SPFA,有点思路了。

还是不行,问了下文心GPT,说优先队列是封闭数据结构没法修改,而且还得重新压入队列,还提供了vector这种优先队列,又引入一堆东西,我就想像“见###5,重新学了下SPFA”里的持续高潮博客那样,能更新,╮(╯▽╰)╭唉,老老实实写der儿的那种吧

T

以上是GPT的回答。

 

写代码的时候不小心换了更新操作的代码的位置,即下面的edge.to和edge.dis这两行

for(int i=0;i<G[edge.to].size();i++){//开始更新
    edge.from=G[edge.to][i].from;
    edge.dis=G[edge.to][i].dis;
    edge.to=G[edge.to][i].to;
    q.push(edge);
}

edge.to就改变了,而且for循环里的edge.to也会变,所以应该先设置个变量存起来

两个错误:

一个是更换两行导致死循环,一直找下一个的to,

一个是for那使得edge.to跟着变了

然后写了个一样的错误的,这块坑好大

for(int i=0;i<G[edge.to].size();i++){//开始更新
    int from=G[edge.to][i].from;
    int to=G[edge.to][i].to;
    int dis=G[edge.to][i].dis;
    
    edge.from=from;
    edge.dis=dis;
    edge.to=to;
    cout<<"!"<<edge.from<<" "<<edge.to<<" "<<edge.dis<<endl;
    q.push(edge);
}

从上一篇学到可以直接压入 q.push(G[edge.to][i]); 

 

AC代码

 1 #include<stdio.h>
 2 #include<string.h>
 3 #include<iostream>
 4 #include<vector>
 5 #include<queue>
 6 using namespace std;
 7 #define MAX 100
 8 struct Edge{
 9     int from;
10     int to;
11     int dis;
12 //dis记录的并不是某点距离to点的最短距离,
13 //记录的是已染色的点距离to的最短距离
14 //只不过最初只有顶点1染色了,所以初始赋值是顶点1距离其他点的距离作为dis,一定要搞清,
15 //而且一开始让1染色也是因为代码好写,算法思想是:让任意点先染色都行,
16 //比如第五个点作为顶点先染色,那就赋值的时候让第五个点到它能连接的点作为dis初始值
17 }edge;
18 //普里姆堆优化不需要lowdis数组,凡事像我之前写lowdis数组的都是没理解算法思想,dis就已经是lowdis了
19 vector<Edge>G[MAX];
20 bool operator<(Edge a, Edge b){
21     return a.dis>b.dis;
22 }
23 int N;
24 int is_in[MAX];
25 int main()
26 {
27 //    freopen("zhishu.txt","r",stdin);
28      while(scanf("%d",&N)!=EOF && N){
29 
30         memset(G,0,sizeof(G));
31         memset(is_in,0,sizeof(is_in));
32 
33         for(int i=0; i<N*(N-1)/2; i++){
34             int a,b,c;
35             scanf("%d%d%d",&a,&b,&c);
36             edge.from=a;
37             edge.to=b;
38             edge.dis=c;
39             G[a].push_back(edge);
40 
41             edge.from=b;
42             edge.to=a;
43             edge.dis=c;
44             G[b].push_back(edge);
45         }
46         priority_queue<Edge>q;
47         while(!q.empty())//常规清空操作
48             q.pop();
49         for(int i=0; i<G[1].size(); i++){
50             edge.from=G[1][i].from;
51             edge.to=G[1][i].to;
52             edge.dis=G[1][i].dis;
53 
54             q.push(edge);
55         }
56          is_in[1]=1;//顶点1加入,染色
57 
58 
59         //开始跑普里姆堆优化
60         int ans=0;
61         while(!q.empty()){
62             edge=q.top();
63             q.pop();
64 //           if(is_in[edge.to]==1)//is_in[edge.from]一定是1
65 //                continue;
66 //          妈的is_in[edge.from]不一定是1,可能from是0,to是1
67             if(is_in[edge.from]==1 && is_in[edge.to]==1)
68                 continue;
69 
70             is_in[edge.from]=1;
71             is_in[edge.to]=1;
72 
73             ans+=edge.dis;
74 //            cout<<"#"<<ans<<endl;
75 //            cout<<"@"<<edge.from<<" "<<edge.to<<" "<<edge.dis<<endl;
76             for(int i=0;i<G[edge.to].size();i++){//开始更新
77 
78 //                cout<<"!"<<edge.from<<" "<<edge.to<<" "<<edge.dis<<endl;
79                 q.push(G[edge.to][i]);
80             }
81         }
82         cout<<ans<<endl;
83     }
84 }

感觉之前那个博客写的那么顺利完全就是凭借模板印象,根本没透彻理解。

 

回顾之前博客

0、之前博客里的这句话 lowcost[ne.to] > ne.dis) ,确实是个剪枝

1、感觉之前博客写少了 is_in[now.to]=1; 但其实没有,在压入的时候他多个判断,仔细想想上一个博客的 (is_in[ne.to]==0),也算一个剪枝

2、从之前博客学到可以直接压入 q.push(G[edge.to][i]);

学着改了下,提高了100ms

 

至此本题OVER

最小生成树OVER

 

###1:关于堆优化里的重载

struct nod
{
    int from, to, dis, ju;
     bool operator<(const nod x) const{
        return dis > x.dis;
     }
 };

回顾之前一直没解决的问题,百度文心GPT傻逼垃圾玩意,解释的狗屁不通,无奈用Google,可是stackoverflow网页,谷歌翻译一直失效,找了个解决办法,但没有host文件,又去找解决办法,(注意是用管理员权限打开cmd),等上个3min,就会自动弹出host文件了。麻痹的没有一个好使的,英文我又看不懂~~~~(>_<)~~~~》之前总结的重载运算符

不知道咋冒出来的WPS翻译,先关掉了,留个设置链接(居然没法超链接):

chrome-extension://blegnhaaimfcklgddeegngmanbnfopog/index.html#/options?page=translator

涉及图片的:

chrome-extension://blegnhaaimfcklgddeegngmanbnfopog/index.html#/options?page=ocr

插件

 

在重新理解下吧,首先对于大顶堆有两种写法,

其一: 

struct Node{
    int num;
    int weight;
};
bool operator<(Node a ,Node b){
    return a.weight>b.weight;
}

这个重载了小于号,但其实是我们理解的那个大于号,至于为什么是大于号我问了下GPT

 1 Q:这样写的a和b也没有顺序怎么定大小啊
 2 
 3 A:在C++中,当我们为一个类型(比如你的Node类型)定义了比较操作符(如小于操作符<)时,我们实际上是在为这个类型的对象指定一个排序的“顺序”。这个顺序是根据我们在比较操作符中定义的逻辑来确定的。
 4 
 5 以你的Node类型为例,如果我们这样定义小于操作符:
 6 
 7 cpp
 8 Copy Code
 9 bool operator<(Node a, Node b){
10     return a.weight < b.weight;
11 }
12 这意味着我们根据weight属性的值来确定Node对象之间的大小关系。具体来说,如果a的权重小于b的权重,那么a就被认为是“小于”b的。
13 
14 现在,关于你提到的“a和b也没有顺序怎么定大小”的问题:实际上,通过定义这样的比较操作符,我们已经为Node对象指定了一个明确的排序顺序。在这个顺序中,权重较小的对象排在前面,权重较大的对象排在后面。
15 
16 这样,当我们需要对Node对象进行排序时(比如使用std::sort函数),C++标准库就会根据我们定义的比较操作符来确定对象之间的顺序。
17 
18 再举个例子,假设你有两个Node对象node1和node2,并且node1.weight = 5,node2.weight = 10。根据我们定义的比较操作符,node1会被认为是“小于”node2的,因为5 < 10
View Code

这个其实就是定了一个顺序,a>b,再进一步说为什么重载小于号要用大于的去弄,是因为我们要用大顶堆来排升序,至于底层优先队列的堆优化堆排序代码怎么跟这个重载符号结合的,有机会学深了再去看看底层代码吧。sort那个cmp也是

其二

struct nod
{
    int from, to, dis, ju;
    bool operator<(const nod x) const{
        return dis > x.dis;
    }
};

这个是写在了结构体里,我试了下单独拿出来不行,上面那个是单独拿出来的写法,因为貌似省略了个参数,比较操作实际上是在比较,“当前”对象的dis成员变量,和参数对象的dis成员变量。

那照着这个能正确的代码强行解释就是也是个大顶堆,当前的比参数对象的dis成员变量大。麻痹的全网到处找不到解释清楚的 大小堆 重载运算符 这个事

###2:东西一旦写博客写下来就没有单独思考深刻,写博客肤浅

###3:赋上几个快捷键,给新手,用codeblock这么久还没写过快捷键的事:(打断点记得单击还是f5从来没用过)

fn+f9编译并运行(记得最开始是DEVC++还是啥,有个f8f9f10,分别编译,运行,编译+运行,感觉没必要,直接编译+运行)

有报错就fn+f2打开下面的报错

win+方向键分屏

alt+tap切换软件

ctrl+tab切换当前软件的标签页

alt+上下是codeblock里的,将某行调整至某位置

全选、复制粘贴、撤销不说了

shift+方向键选中某行

选中某行后ctrl+shift+X是取消注释,+C是加注释

某行选中后,按下ctrl+shift+X,变为整体选中,可以按tab进行整体增加一个tab位置

ctrl+R替换

右键“format use”那个是更好的阅读模式

###4:根据之前的经验

如果是vector<Node>G用 G.clear() ,就是单纯的清空
如果是vector<Node>G[MAX]用 memset 0 ,我估计应该是size为0
我也不知道为啥,GPT答复如下:

Q:如果是vector<Node>G用G.clear()初始化
     如果是vector<Node>G[MAX]用memset 0为啥

A:这个问题挺有意思的,让我来给你解答一下!

首先,vector<Node> G 是一个动态数组,它存储的是 Node 类型的对象。
当你使用 G.clear() 时,你实际上是在清空这个动态数组中的所有元素,
使其大小变为0,但 G 这个 vector 对象本身仍然存在。这并不意味着你“初始化”了 G,
而是清空了它的内容。真正的初始化通常是在声明 vector 时进行的,比如 vector<Node> G; 或者 vector<Node> G = vector<Node>();。

然后,vector<Node> G[MAX] 是一个数组,
其中每个元素都是一个 vector<Node> 类型的对象。
这里,MAX 是一个常量,表示数组的大小。当你想要“初始化”这个数组时,
你实际上是想要将数组中的每个 vector<Node> 对象都设置为一个空的状态。
但是,你不能直接使用 G.clear(),因为 G 是一个数组,
不是一个单一的 vector 对象。所以,你需要遍历这个数组,
并对每个元素调用 clear() 方法。
不过,如果你想要的是将每个 vector<Node> 对象都设置为一个“全新”的状态(不仅仅是清空内容,还要确保内存等都被正确初始化),
那么使用 memset 就不合适了,因为 memset 只是将内存设置为0或某个特定的值,
它并不了解 vector 的内部结构和初始化逻辑。

然而,在实际编程中,如果你只是想简单地清空每个 vector<Node> 对象的内容,
并且你确定这样做是安全的(即没有破坏 vector 的内部状态),
你可以使用 memset 来将数组的内存区域设置为0。
但请注意,这种做法并不是标准的 vector 操作方式,
它依赖于 vector 的实现细节(比如内存布局),
因此可能会在不同的编译器或不同的 vector 实现中表现出不同的行为。

更稳妥的做法是使用循环来遍历数组,并对每个元素调用 clear() 方法。例如:

cpp
Copy Code
for (int i = 0; i < MAX; ++i) {
    G[i].clear();
}
这样,你就可以确保每个 vector<Node> 对象都被正确地清空了。

最后,需要强调的是:memset 函数通常用于设置原始内存区域的值,
而不是用于初始化C++标准库中的复杂对象(如 vector)。
在使用 memset 时,你需要非常小心,确保不会破坏对象的内部状态或导致未定义行为。

希望这个解释能帮到你!如果你还有其他C++相关的问题,随时都可以问我哦!
View Code

之前提过:博客博客(搜“用memset0了,但不可以吧?”)、wjy666ljcljc

没有一个能解释清楚的,先搁置

###5:放一个除了讲解像喝多了一样剩下哪都好的博客,再也懒得看的SPFA,也好像忘得一干二净,为什么讲解都是那么废物的图,最能体现处理负权,区别于迪杰斯特拉的图是,眼前显然的最短路并不是真正的答案那种啊:

A B 5
A C 7
C D 100
D B -107

有些傻逼博主解释SPFA连个负权的图都没有,解释你妈呢?怎么让人理解它和迪杰斯特拉的区别啊艹

突然发现之前刷的SPFA都是假题,从来没遇到过需要vis数组的,重新学SPFA,清清爽爽,相当持续高潮的好博客,简单明了。代码超级棒,解释超级棒,例子也超级棒,

加vis或者说st数组判断是否存在于队列中,其实没啥特别的,就是防止重复加入队列而已,但如果已经加入了队列,但需要更新,是会正常更新的

###6:

 

最选了最小,没更新,而是直接加入,即该更新的那个边,更新前后的权值都在队列里,只不过,取短的先弹出了,然后把路“封死”,即点染色,那等长的再弹出的时候,就不会有任何操作了
posted @ 2024-12-10 04:29  GerJCS  阅读(12)  评论(0)    收藏  举报