C++学习笔记2(含vscode配置C++.vscode文件、八数码问题代码注释)

八数码:
https://www.acwing.com/solution/content/35528/
主要参考的上面链接的题解写的注释,便于自己理解记的笔记。
曼哈顿距离:
这里只是对于二维的情况进行的解释,曼哈顿距离为初末位置上面两点之间的距离坐标绝对值的差的和。
d i s t a n c e = ∣ x j − x i ∣ + ∣ y j − j i ∣ distance=|x_j-x_i|+|y_j-j_i| distance=xjxi+yjji
下面为一个八数码问题利用曼哈顿距离来估计次数的函数:

int f(string m)//估计函数
{
    int dt=0;
    for(int i=0;i<9;i++)//这里1~8对应的下标为0~7
    if(m[i]!='x')
    {
        int t=m[i]-'1';//对应下标
        dt=dt+abs(i/3-t/3)+abs(i%3-t%3);//曼哈顿距离
    }
    return dt;//返回总曼哈顿距离
}

首先其中的dt表示的是这次移动过程中,水平方向空格移动的次数加上竖直方向上面空格移动的次数。
我们可以发现首先是根据的该点是否为空格来判断的,如果不是的话就把t减去1来获取其当前情况下的下标,获取完之后跟原本的01234567来进行对比计算,最终获得这个差值,作为曼哈顿距离的具体数值,也就是恢复为原本的:
123
456
78x
所需要的具体次数。

优先级队列:
priority_queue(STL priority_queue)
构造队列可以首先先构造一个普通的字符串数组:

#include <iostream>

using namespace std;

int main()
{
   std::string wrds[] { "one", "two", "three", "four"};
   for(int i=0;i<=wrds->length();i++)cout << wrds[i] << endl;
   return 0;
}

在将字符串数组放到priority_queue的时候,可以传入两个参数,std::begin(wrds),std:: end(wrds)。这两个参数对应的分别是数组开始的地址跟结束后的地址。
封装及输出代码如下:

#include <iostream>
#include <queue>

using namespace std;

int main()
{
    string wrds[] { "one", "two", "three", "four"};
    priority_queue<string> words {begin(wrds),end(wrds)};
    while(!words.empty()){
        cout <<words.top()<<endl;
        words.pop();
    }    
    return 0;
}
cout:
two
three
one
four

我们可以发现顺序发生变化了。
对于priority_queue主要的操作方法有如下的几种,主要参考的是下面的链接:
C++ priority_queue(STL priority_queue)用法详解
push(const T& obj):将obj的副本放到容器的适当位置,这通常会包含一个排序操作。
push(T&& obj):将obj放到容器的适当位置,这通常会包含一个排序操作。
emplace(T constructor a rgs…):通过调用传入参数的构造函数,在序列的适当位置构造一个T对象。为了维持优先顺序,通常需要一个排序操作。
top():返回优先级队列中第一个元素的引用。
pop():移除第一个元素。
size():返回队列中元素的个数。
empty():如果队列为空的话,返回true。
swap(priority_queue& other):和参数的元素进行交换,所包含对象的类型必须相同。
这里由于输入有问题,所以需要打开cmd输入。
查阅资料发现,配置launch文件就可以。
弄了大半天,发现launch里面讲究挺多,不过还算简单:

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
    {
        "name": "(gdb) 启动",
        "type": "cppdbg",
        "request": "launch",
        "program": "${fileDirname}/${fileBasenameNoExtension}.exe",
        "args": [],
        "stopAtEntry": false,
        "cwd": "${fileDirname}",
        "environment": [],
        "externalConsole": true,
        "MIMode": "gdb",
        "miDebuggerPath": "D:/mingw64/bin/gdb.exe",
        "setupCommands": [
            {
                "description": "为 gdb 启用整齐打印",
                "text": "-enable-pretty-printing",
                "ignoreFailures": true
            },
            {
                "description":  "将反汇编风格设置为 Intel",
                "text": "-gdb-set disassembly-flavor intel",
                "ignoreFailures": true
            }
        ],
        "preLaunchTask": "C/C++: g++.exe 生成活动文件",
    }
    ],

}

首先是需要跟task.json链接,用哪个prelaunchtask写上对应的label就行,然后就是program需要加上你要运行的文件的exe,之后就是将externalConsole改成true即可。
具体的配置json文件方法这里小结一下吧:
在这里插入图片描述
ctrl+shift+D打开调试,选中创建launch.json
选中其中的gdb来进行创建configuration,然后把里面的这三个地方稍微改下:
在这里插入图片描述
filedirname就是当前文件夹路径,然后filebasenamenoextension表示的就是去掉后缀的文件名,后面加个exe对应的就是task生成的exe的文件名字,基本这样就能正常打开命令行了。

调用其他的库的时候需要稍微改一下,最开始我确实这样想的,但是看到一个大佬的文章,https://blog.csdn.net/qq_43700779/article/details/121891677
将其中的动态链接库libstdc++ -6.dll 放到了c:/windows/system32中,能够正常的运行很多指令了,也不会出现闪退了。
之后继续来进行八数码问题代码的测试,首先是其中的输入:

#include<iostream>
#include<string>
using namespace std;

int main()
{
    string start,x,c;
    while(cin>>c)//这样输入可以忽视空格
    {
        start+=c;
        if(c!="x") x+=c;
        cout<<c<<endl;
    }
    cout<<start<<endl;
    return 0;
}

我这里是直接输入了原本的题目,然后输入完之后回车+ctrl+c结束,最终的出来的结果确实是没有空格出现,很神奇。
2 3 4 1 5 x 7 6 8
2
3
4
1
5
x
7
6
8
23415x768
这里的话原理确实不太懂,每次的C输入进来好像都是一个类似于字符的数,一直输入的话好像也不会怎么样,而且那个x我确实也没输出出来,因为感觉在这里用不到。之后也输出出来了,其实就是把x给去掉了,别的没有区别,我觉得这个不保存空格的输入方式可以稍微学一手。

逆序对数目判断:

int res=0;//统计逆序对的数量
    for(int i=0;i<8;i++)
     for(int j=i+1;j<8;j++)
      if(x[i]>x[j]) 
       res++;

这里的话主要就是利用两个之间是否有逆序变化来判断是否有解的,如果是偶数的话一般就是有解,否则就是没有解。
为了便于看到迭代的过程,利用cout输出结果:

#include<iostream>
#include<string>
using namespace std;

int main()
{
   string start="23415x768";
   string x="23415768";
   
   int res=0;//统计逆序对的数量
   for(int i=0;i<8;i++)
       for(int j=i+1;j<8;j++)
       {
           if(x[i]>x[j]){
               res++;
           }
           cout<<"the index i is:"<<i<<"\t";
           cout<<"the index j is:"<<j<<endl;
       }
   system("pause");
   return 0;
}

经过对i j输出,我们可以看出其迭代的过程
the index i is:0 the index j is:1
the index i is:0 the index j is:2
the index i is:0 the index j is:3
the index i is:0 the index j is:4
the index i is:0 the index j is:5
the index i is:0 the index j is:6
the index i is:0 the index j is:7
the index i is:1 the index j is:2
the index i is:1 the index j is:3
the index i is:1 the index j is:4
the index i is:1 the index j is:5
the index i is:1 the index j is:6
the index i is:1 the index j is:7
the index i is:2 the index j is:3
the index i is:2 the index j is:4
the index i is:2 the index j is:5
the index i is:2 the index j is:6
the index i is:2 the index j is:7
the index i is:3 the index j is:4
the index i is:3 the index j is:5
the index i is:3 the index j is:6
the index i is:3 the index j is:7
the index i is:4 the index j is:5
the index i is:4 the index j is:6
the index i is:4 the index j is:7
the index i is:5 the index j is:6
the index i is:5 the index j is:7
the index i is:6 the index j is:7
逆序对具体数学原理暂时不知道,感觉对于做题没太大影响,就暂时不去想了。总之就是,如果说它是一个偶数的话就是可以进行的,如果不是的话就是不可以解出来的。具体的数学原理之后有时间再去研究。

接下来看下大头,广度搜索算法部分:

string bfs(string start)
{
    string end="12345678x";//终点

    unordered_map<string,int> d;//存储距离
    priority_queue<PIS, vector<PIS>, greater<PIS>> heap;//小根堆,将元素的估计终点距离从小到大排序
    unordered_map<string,pair<string,char>> last;//存储一个元素由哪种状态,经过哪种操作得来,跟前面几题一样

    heap.push({f(start),start});//加入起点
    d[start]=0;//起点到起点的距离为0
    //要将操作数组与坐标变化数组一一对应
    char oper[]="udlr";
    int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};

    while(heap.size())
    {
        auto t=heap.top();//队头
        heap.pop();//弹出
        string state=t.y;//记录

        if(t.y==end) break;//终点出列的话就退出

        int x,y;//查找x的横纵坐标
        for(int i=0;i<9;i++)
        if(state[i]=='x')
        {
            x=i/3,y=i%3;
            break;
        }

        string init=state;
        for(int i=0;i<4;i++)
        {
            int a=x+dx[i],b=y+dy[i];
            if(a<0||a>=3||b<0||b>=3) continue;//越界就跳过
            swap(state[a*3+b],state[x*3+y]);//交换下标位置
            if(!d.count(state)||d[state]>d[init]+1)//如果没有被记录或者小于记录值
            {
                d[state]=d[init]+1;//更新距离
                heap.push({f(state)+d[state],state});//加入堆中
                last[state]={init,oper[i]};//标记由哪种状态转移而来,并且记录执行的操作
            }
            state=init;//因为要扩展到四个方向,所以要还原
        }
    }

    string ans;
    //跟前面几题原来相同
    while(end!=start)
    {
        ans+=last[end].y;
        end=last[end].x;
    }
    reverse(ans.begin(),ans.end());//将其反转
    return ans;
}

首先是其中的终止状态,对应的是一个恢复后的八数码表格,然后是三个初始化后的数据结构:
unordered_map<string,int> d;
建立一个hash表,key:string,value:int;
priority_queue<PIS, vector<PIS>, greater<PIS>> heap;
建立一个小根堆,这种设计的方法主要是根据的这个博主地教程理解的,感谢哈:
priority_queue
目前的话没有打算深入去看看这个小根堆具体的设置方法,之后再补下哈哈。
unordered_map<string,pair<string,char>> last;
同上面哪个定义一个hash表,结构为:
类似于:
{string:{string:char}}的一个结构。

下面慢慢的进行下原本的函数的初始调试:
首先先对其中的一些函数进行初始整合:
f函数 基本结构定义 小根堆插入弹出队头显示
在试图观察我这个heap top的时候出现问题了,cout的输出变量方法不清楚导致的。
之后发现不是因为cout输出方式用错了,是因为pair调用的时候应该用.first等参数,注意不加括号!
只输入了一个start之后发现曼哈顿估计的距离是9,输出两个变量第一个是估计的曼哈顿距离,第二个是当前状态。
heap_top后的输出为:
23415x768
9
对上述三个初步整合的元素代码进行记录:

#include <iostream>
#include <unordered_map>
#include <queue>

using namespace std;

//Manhattan距离估计
int f(string m)
{
    int dt=0;
    for(int i=0;i<9;i++)
    {
        if(m[i]!='x')
        {
            int t=m[i]-'1';
            dt=dt+abs(i/3-t/3)+abs(i%3-t%3);
        }
    }    
    return dt;
}

int main()
{
    string start="23415x768";
    // 需要的结构体声明
    /*
    d:距离表,对应的当前状态与变换次数;
    heap:小根堆,存放估计的次数,排序,距离最小的在最上面;
    last:记录元素经过的状态以及次数。
    */
    unordered_map<string,int>d;
    priority_queue<pair<int,string>,vector<pair<int,string>>,greater<pair<int,string>>>heap;
    unordered_map<string,pair<string,char>>last;

    heap.push({f(start),start});
    auto t=heap.top();
    cout<<"heap_top后的输出为:"<<endl;
    cout<<t.second<<endl;
    cout<<t.first<<endl;
    return 0;
}

把剩下的部分进行了一下注释,大致意思明白了,除了一些是否能够求解出来的情况的判断,其他基本可以正常实现:

#include <iostream>
#include <unordered_map>
#include <queue>
#include <algorithm>

using namespace std;

//Manhattan距离估计
int f(string m)
{
    int dt=0;
    for(int i=0;i<9;i++)
    {
        if(m[i]!='x')
        {
            int t=m[i]-'1';
            dt=dt+abs(i/3-t/3)+abs(i%3-t%3);
        }
    }    
    return dt;
}

int main()
{
    // 定义初始终止状态
    string start="23415x768";
    string end="12345678x";

    // 需要的结构体声明
    /*
    d:距离表,对应的当前状态与变换次数;
    heap:小根堆,存放估计的次数,排序,距离最小的在最上面;
    last:记录元素经过的状态以及次数。
    */
    unordered_map<string,int>d;
    priority_queue<pair<int,string>,vector<pair<int,string>>,greater<pair<int,string>>>heap;
    unordered_map<string,pair<string,char>>last;

    // 定义操作名称
    char oper[]="udlr";

    // 定义操作,左右,上下移动空格 
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};

    // 初始化操作
    heap.push({f(start),start});
    d[start]=0;

    while(heap.size()){
        auto t=heap.top();
        heap.pop();

        // 判断当前的状态是否是目标状态,到达目标状态终止
        string state=t.second;
        if(state==end)break;
        
        // 设定初始状态为当前状态
        string init=state;

        // 确定空格'x'的坐标,包括横纵坐标,x为横坐标,y为纵坐标.
        int x,y;
        for(int i=0;i<9;i++)
        {
            if(state[i]=='x')
            {
                x=i/3;
                y=i%3;
                break;
            }            
        }

        // 生成最多四个当前状态的子状态
        for(int i=0;i<4;i++)
        {
            // 获取新状态的坐标,并判断状态是否存在
            int a=x+dx[i];
            int b=y+dy[i];

            // 判断边界,越界不添加状态
            if(a<0||a>2||b<0||b>2)continue;

            // 如果满足条件就转换状态
            swap(state[a*3+b],state[x*3+y]);

            // 判断原本的hash表是否存在该状态,如果存在的话判断是否小于记录的数值(能用更短的步数到达这一步)
            if(!d.count(state)||d[state]>d[init]+1)
            {
                d[state]=d[init]+1;
                heap.push({f(state)+d[state],state});
                last[state]={init,oper[i]};
            }
            state=init;
        }
    }

    // 类似于回溯操作 
    string ans;
    while(end!=start)
    {
        ans+=last[end].second;
        end=last[end].first;        
    }
    reverse(ans.begin(),ans.end());
    cout<<ans<<endl;
    return 0;
}

封装之后的代码如下所示:

#include <iostream>
#include <unordered_map>
#include <queue>
#include <algorithm>

using namespace std;

//Manhattan距离估计
int f(string m)
{
    int dt=0;
    for(int i=0;i<9;i++)
    {
        if(m[i]!='x')
        {
            int t=m[i]-'1';
            dt=dt+abs(i/3-t/3)+abs(i%3-t%3);
        }
    }    
    return dt;
}

string bfs(string start,string end)
{
     // 需要的结构体声明
    /*
    d:距离表,对应的当前状态与变换次数;
    heap:小根堆,存放估计的次数,排序,距离最小的在最上面;
    last:记录元素经过的状态以及次数。
    */
    unordered_map<string,int>d;
    priority_queue<pair<int,string>,vector<pair<int,string>>,greater<pair<int,string>>>heap;
    unordered_map<string,pair<string,char>>last;

    // 定义操作名称
    char oper[]="udlr";

    // 定义操作,左右,上下移动空格 
    int dx[4]={1,-1,0,0};
    int dy[4]={0,0,1,-1};

    // 初始化操作
    heap.push({f(start),start});
    d[start]=0;

    while(heap.size()){
        auto t=heap.top();
        heap.pop();

        // 判断当前的状态是否是目标状态,到达目标状态终止
        string state=t.second;
        if(state==end)break;
        
        // 设定初始状态为当前状态
        string init=state;

        // 确定空格'x'的坐标,包括横纵坐标,x为横坐标,y为纵坐标.
        int x,y;
        for(int i=0;i<9;i++)
        {
            if(state[i]=='x')
            {
                x=i/3;
                y=i%3;
                break;
            }            
        }

        // 生成最多四个当前状态的子状态
        for(int i=0;i<4;i++)
        {
            // 获取新状态的坐标,并判断状态是否存在
            int a=x+dx[i];
            int b=y+dy[i];

            // 判断边界,越界不添加状态
            if(a<0||a>2||b<0||b>2)continue;

            // 如果满足条件就转换状态
            swap(state[a*3+b],state[x*3+y]);

            // 判断原本的hash表是否存在该状态,如果存在的话判断是否小于记录的数值(能用更短的步数到达这一步)
            if(!d.count(state)||d[state]>d[init]+1)
            {
                d[state]=d[init]+1;
                heap.push({f(state)+d[state],state});
                last[state]={init,oper[i]};
            }
            state=init;
        }
    }

    // 类似于回溯操作 
    string ans;
    while(end!=start)
    {
        ans+=last[end].second;
        end=last[end].first;        
    }
    reverse(ans.begin(),ans.end());
    cout<<ans<<endl;
    return ans;
}

int main()
{
    // 定义初始终止状态
    string start="23415x768";
    string end="12345678x";
    
    // 获取去除空格"x"后的字符串'23415768'
    string x="";
    for(int i=0;i<9;i++)if(start[i]!='x')x+=start[i];
    cout<<x<<endl;

    //统计逆序对的数量,如果逆序对为奇数,就不可能抵达终点。
    int res=0;
    for(int i=0;i<8;i++)for(int j=i+1;j<8;j++)if(x[i]>x[j])res++;
    if(res%2) printf("unsolvable\n");

    // 广度优先搜索
    bfs(start,end);
   
    return 0;
}

关键代码总结:
在这里插入图片描述

再次感谢:
AcWing 179. 八数码
作者: E.lena

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值