排序之基数排序:LSD & MSD

LSD原理:从最低位到最高位,将每个数字放入这一位数对应的桶中,再依次按顺序倒出,即优先度越高的元素(更高位的数),越在后面排序。

LSD代码:(使用了STL,提高输入数据大小、长度的灵活性)

#include <iostream>
#include <vector>
using namespace std;
const int DIGITS = 10;
int max(vector<int>);
int count_digit(int);
int get_digit(int num, int cnt);

int main()
{
	vector<int> datas = { 36,5,16,98,1123,1523,1698,5,98,95,47,32,36,48,10,423,455,127,432,123,834,678,176 };

	for (int digit = 0; digit < count_digit(max(datas)); digit++)
	{
		vector<int> t[DIGITS] = {};
		for (auto num : datas)
			(t + get_digit(num, digit))->push_back(num);
		datas.clear();
		for (int i = 0; i < DIGITS; i++)
			for (auto num : t[i])
				datas.push_back(num);
	}

	for (auto num : datas)
		cout << num << ' ';

	return 0;
}

int max(vector<int> datas)
{
	int max = 0;
	for(auto num : datas)
		max = num > max ? num : max;
	return max;
}

int count_digit(int n)
{
	if (n < 10)
		return 1;
	else
		return count_digit(n / 10) + 1;
}

int get_digit(int num, int cnt)
{
	if (cnt == 0)
		return num % 10;
	else
		return get_digit(num / 10, cnt - 1);
}

利用同样的办法,可以对所有输入的字符串以ASCII码顺序来排序:

#include <iostream>
#include <vector>
using namespace std;
const int DIGITS = 128; //ASCII码最大值为127
int max_digit(vector<string>); //获得所有字符串中最长的长度
char get_digit(string num, int cnt); //获得从右起第cnt位字符
int main()
{
    vector<string> datas = {};
    int count;
    string str;
    cin >> count;
    for (int i = 0; i < count; i++)
    {
        cin >> str;
        datas.push_back(str);
    }
    //对每一位字符倒入、倒出桶
    for (int digit = 0; digit < max_digit(datas) + 1; digit++)
    {
        vector<string> t[DIGITS] = {};
        for (auto num : datas)
            (t + (int)get_digit(num, digit))->push_back(num);
        datas.clear();
        for (int i = 0; i < DIGITS; i++)
            for (auto num : t[i])
                datas.push_back(num);
    }

    for (auto num : datas)
        cout << num << ' ';
    return 0;
}
int max_digit(vector<string> datas)
{
    int max = 0;
    for (auto num : datas)
        max = num.length() > max ? num.length() : max;
    return max;
}

char get_digit(string num, int cnt)
{
    if (cnt > num.length()) //如果访问的位不存在,用ASCII码最小的NULL填补
        return NULL;
    return num[num.length() - cnt];
}

MSD原理:(待更新)

断章取义+字义不明=表意偏差

——聊聊社群给路人留下坏印象的原因之一

首先先看这么一个简单的句式:

 “A是B。”

这是一个十分基本的动词谓语主谓句。如果按照这个句式,并且不带上任何状语,根据A和B的词性,可以理解成凡是满足A条件、或者属于A群体、或者具备A属性的所有个体,无一例外地,满足B条件、或者属于B群体、或者具备B属性。

  1. 举个例子:偷窃是违法行为。
    • 很容易理解,这句话是指,凡是符合“偷窃”的定义的行为,都是违法的行为。这里的“偷窃”倘若按照法律中的定义的话,这句话是正确的。同时一般地,也可以推导出下一个结论:凡是满足了“偷窃”这个条件的人,都是做了违法行为的人。
  2. 再举个例子:吸血鬼是不存在的。
    • 可以同样理解,这句话是指,凡是符合吸血鬼定义的生物,都是不存在于世界上的。从字面的理解,可以将某作品中对吸血鬼的定义作为此句中的定义,“不存在”理解为不能在我们生活着的这个地球上找到。那么这样看来,这句话是正确的,世界上没吸血鬼(姑且这样认为吧)。
  3. 最后一个例子:今天的晚饭是西红柿炒鸡蛋。
    • 这里的A限定成为了唯一的事物:今天的晚饭。但这句话并不是一个正确性绝对的称述,取决于不同时间、不同人物,这只是对某一件特殊事件的称述,而其特殊性没有被限定。只有拥有了足够的额外限定,这个时候正确性才能被确定。

那么这句话使人们的言论发生了什么偏差呢?

断章取义

语句往往是带有上下文语境的,断章取义常常摧毁一个语句。继续聊聊例子吧。如果在例(3)中,加上完整的背景:

  • 到晚上了,大家在萨〇亚里聚餐。
  • 你翻开了萨〇亚的菜单,发现菜单里没有西红柿炒鸡蛋。
  • 你旁边的十分要好的朋友说:“今天的晚饭是西红柿炒鸡蛋。”

这个时候,你方才可以说:

  • “去你的,晚餐怎么可能是西红柿炒鸡蛋。”

可是很不幸运地,这句话被某个听者单独提了出来,作为了你的“名言”。许多本来不怀恶意的人,因为不了解背景,可能会这样发表看法:

  • “晚餐怎么不能是西红柿炒鸡蛋了?”
  • “你开除了西红柿炒鸡蛋的食物籍?”
  • “这人估计就是不喜欢吃西红柿炒鸡蛋吧,怎么还非要别人赞同的。”
  • “不喜欢就不喜欢吧,怎么还对别人说不尊敬的话了?”

最后,你被迫在各社交媒体,向所有产蛋的鸡和结西红柿的果树公开道歉了,因为这是最快最方便的解决你的个人形象危机问题的办法;向所有人解释事件的背景,反而效率低、并且他人不相信、甚至认为你在为自己辩解。

当然,以上只是一个玩笑话了,现实中应该挺难见到对西红柿炒鸡蛋会大做文章的人(存疑?)。不过很多看起来非常奇怪的论断,往往作用机理就是这样的断章取义。

例如,现代人在引用他人的话的时候,如果是为了表现他人言论的某种观点,最热衷于使用的方法之一是QQ聊天记录选段。较新版本的QQ甚至具备了隔行选取部分聊天记录截图的功能。为了佐证自己对某人的论断,出于人之常情,往往截取的是对自己有利的部分;而更加讲究效率,或者是偷了个懒的人,对言论背景的截取就更少了。致使,QQ聊天记录是最能歪解他人真实观点的工具之一(当然不是指但凡聊天记录就是歪解,而是说十分容易用于歪解)。而聊天记录往往是人们确信不疑的,毕竟,每一句话、每一个字、每一个符号,都是聊天记录的主人亲手输入的。

因此,也同时小小的倡议大家,对聊天记录不应随意照单全收。主观意识中认为“就这句话,不带上下文,也可以看出这个人有XX的特点”的想法,都有可能最终证实是不符合现实状况的。

在理想情况中,人面对一个言论的截取片段,都应该带上十足的存疑精神,只有深入调查过背景,才能拥有发表一个实事求是的评价的必要条件。

然而事实上不可能有人对任何想发表评论的事情都能做到完全调查清楚,时间有限,精力也有限,事件的一些真相也并没有办法为人所知。因此,个人给出以下一些小建议,欢迎各位指出不足、添删改查:

  • 对事件,尽量不要在仅掌握少量情报的时候,就发表明显超过可推测事实的决断性言论。
  • 对他人立场鲜明、直指矛头的观点,尽量不要盲信盲从、盲目跟风,最好带上自己的思考。
  • 对于某些暂时似乎已经盖棺定论的事件,也要做好结局反转的可能性的心理准备。
  • 额外建议:少动怒,多讲理。少扣帽子,多给建议。尽量文明用语,拒绝阴阳讽刺。

字义不明

字义不明往往是另一个使人对言论产生错误理解的原因。不明的表现可能是没有结合上下文的会错意,也有可能是谈及的某物没有自洽的定义,或者发表言论者本人忘记了对自己说的某句话中的某个含义给出在这句话中的定义,等等。

还是看例子,例(2):“吸血鬼是不存在的。”

  • “吸血鬼存在吗?”

如果你朋友冷不丁地问了这么一个问题,你是否会充满了问号?

  • 吸血鬼是什么?靠吸生物血维生的亡灵生物?尽管500岁依然娇小可爱的萝莉?还是比喻残酷欺压人的人?或者是不干活只向别人索取来享乐的人?
  • 存在是怎么存在?就在我们日常生活中?在我昨天安利给他的游戏中?在世界上任何能找到的文学作品中?在大脑的概念中?

这个时候,你可能会丢下一句“你说啥”,让他再详细阐明一下问题。

同样地,很多人在公开发表言论的时候,也像这个问题一样,对自己实际想要论述的概念,没有做好准确的界定。然而不同的是,以下的情况使观点准确表达产生了困难:

  • 某个词在语境中十分偏向于某个概念,让人自然向这方面理解,然而实际想要表达的却和这有偏差;
  • 某个词的概念十分模糊,但是听众缺少能够直接向发言人提问以明确概念的途径;
  • 发言人为了达到渲染情感的作用,特地采用了夸张手法,然而听众没能会意,过大地理解了问题的严重性等;
  • 发言人对词语、或者对想要表达的现象的理解偏差,致使真实观点没能传达给听众;
  • 发言者的整体说话语气,也会使不同听众对某个词的概念产生不同的理解,而发言者本身可能没有意识到这一点;
  • ……

发言人的观点,经由自己的说出来的一个个词语,到听众的耳中,最后解析为大脑中的理解,环节很多,容易产生偏差的地方更多。甚至,每个人的理解也随着每个人的立场不同、参与度不同、潜意识不同而产生变化。因此,想要非常大程度上的提高用词的准确性,难度同样不小。

表意偏差

断章取义和字义不明,往往是言论不能好好地传达的重要因素,也是歪解他人观点的一大武器。

这是很自然的现象,字义不明往往使得一个句子单独列出来无法具有意义,而断章取义又正好割去了让这个单独不达意的句子失去了让他被人理解的最后途径。单独截取出一句话的下场,往往是表达的心意发生了很大的偏差。

然而,对于断章取义,暂时还没有找到很好地主动防卫措施。毕竟,没有办法给每段言论加上一个MD5码永远跟着文章走,同时还方便大家取证。面对他人的断章取义,若要为自己辩白,只能兵来将挡水来土掩,针对他人截取的内容放出更完整的内容来。如果有更好的主动防止他人对自己言论的断章取义的办法,请务必及时教授我。

因此,发言者能做到的,主要只有多检查、多斟酌,站在自己和大多数听众的角度上理解自己的发言是否将会传达自己的真实想法。尤其是影响力大的人物,常常一句话带动的是社群中的整体风气走向。这个时候,如果希望社群健康发展,打个比方,最好拥有一种“被迫”发布准确言论的自觉性,对每一句话负起责来,其重要性不言而喻。

紧接着来看一个案例(内容全属我杜撰,如有雷同,纯属巧合):

圈内有很多乌烟瘴气的地方,例如同好群等等。

——某句可能被数万读者阅读到的言论

这句话一经出现便让人感觉到非常难受,怎么QQ群摇身一变就变成乌烟瘴气的场所了呢?这么多用户就被一竿子打翻一船鸭子了?这像什么话?

首先,我们联系一下上下文。这句话所处的原文是在谈论,社群不是乌托邦,同好圈并不是铁板一块,其中含有不少毒害同好思想、使路人畏不敢入的成分;同时,区别开了IP与IP同好社群,不应该将社群现象都作为IP内容一股脑地接纳。

从这个意义上讲,文章的出发点是好的。其作者想在这句话中表达的意思,也自然是警醒读者对同好群里的各种言谈理性对待。结合前后两句话:

圈子里面并不是每个社交平台都是传达正能量的人在发表言论。

圈内有很多乌烟瘴气的地方,例如同好群等等。

个人最重要的是明辨是非,不要被偏激的看法洗了脑。

——包含先前没有被引用的部分的同义句言论

前后句被引用进来之后,“同好群”的概念也明晰了,这句话中的“同好群”不是普遍概念,而是集合体概念。换句话说,不是指同好群每个都是乌烟瘴气的,而是指同好群并不是一个完全由正能量组成的平台,有的同好群有的时候会产生具有负能量的言论。

再来看另一个案例(同样,事件和评论内容全属我杜撰,如有雷同,纯属巧合):

感觉圈子里混进了一些借势搞事的小鬼。

——对某件被从一方角度客观称述的事件发表的评论

想象一下事件的背景:甲对乙所做的(与社群圈子关联很大的)某事持有否定的看法,然而使用了讽刺的手法,说了让乙认为受到冒犯的话,于是乙把这部分聊天记录展示出来。

  • 首先,该评论者由于不了解发生这些言论的前后背景,直接将个人对个人的看法(或者是比较冒犯的讽刺)上升为了社群这一大批不怀好意的人正在对社群重大事件进行蓄意破坏。
  • 然后,该评论者“想象”了一下事件的背景,认为甲在某丙势力的靠山之下,故意咄咄逼人,贬低乙抬高丙,尽管事件中的人并没有讨好某势力的想法,仅仅是就事论事,只是错在了语言冒犯了人。
  • 最后用上意味不明的“小鬼”一词,实际难以判断字义,可是让人觉得站在了道德的制高点,划清界限的同时,表达了自己的无限憎恨。同时给读者一个固化印象:发表冒犯人的言论的人,是一个不折不扣的破坏分子,在社群的方方面面无恶不作。

虽然该评论者的出发点依然是批评出言冒犯者,劝告大家为人光明磊落,可是有形地贬低了犯错误者,不仅没有起到劝说改正的作用,反而恶语相向、以惹人不快为目的。虽然这确实与人之常情偏离不远,可以理解,但是如果能多加调查背景,好好用词,则可以起到更好的劝说效果。比如评论:“希望圈子里的人端正态度、文明用语。”

那么,表意偏差对社群形象有什么负面影响呢?

这很明了了。就以上两个案例来说,在第一个案例中,倘若路人仅仅看见了被引用出来的中间一句,便会接受到这样的认识:或者是社群中的同好群确实处处乌烟瘴气,让人无法忍受;或者是社群中部分其他平台的人和同好群的人互相掐架……不论是哪一种认识,都将成为将路人拒之社群门外的绊脚石,甚至连带对这个IP产生了敬而远之的看法。

于是,表意准确对具言论影响力的社群中心成员提出了要求。

而在第二个案例中,这一类乍一看情感分明、归纳性强、逻辑通顺的评论往往更容易被人看见。而路人见到之后,不免得被一次次加深对这个社群的负面印象:全是“小鬼”,明争暗夺,乌烟瘴气,人人自危。虽然真正蓄意破坏的人的比例非常非常少,本质善意、出言大意的人才是被扣上帽子的人,可是再联系上确实发生过并且被多次放大的恶性事件,换做谁,都很难再愿从路人转为该IP爱好者。

因此,表意准确也是社群中每一个成员需要尽量做到的发言方式。

所谓好事不出门,坏事传千里。无需重复强调,正是在表意偏差的结果下,外人眼中的社群形象由于各种各样的“怪言怪语”一步步变差。好好说话,好好做人,是对社群每个组成成员的要求。放在社会中的为人处世上,也不失为有道理。

后记

由于个人水平有限,本文中也很可能存在着用词不准、表意不明的地方,在欢迎各位批评指正的同时,也诚心希望各位摆以讲道理的态度和平讨论。

聊聊动态规划之BFS/DFS+剪枝——以滑雪问题为例(下)

下篇接着上篇的思路,对DFS同样进行剪枝

DFS是以深度为优先的搜索方式,先沿着一枝探索到底,然后逐枝从探索过的节点回溯,再探索下一枝。这种情况下,后进先出的栈更适合存储节点信息。同样地,在选取了某个曾经到达过的节点时,判断这枝是否优先度更高,从而保留或者更新枝,达到剪枝的效果。

接着上一段同样滑雪问题的DFS解法代码:

#include <iostream>
#include <stack>
using std::stack;
using std::cout;

const int Length = 5;
const int Height[Length][Length] =
{
	{1, 2, 3, 4, 5},
	{16, 17, 18, 19, 6},
	{15, 24, 25, 20, 7},
	{14, 23, 22, 21, 8},
	{13, 12, 11, 10, 9}
};
int maxsteps[Length][Length] = { 0 };

const int direction[4][2] = { {1,0},{0,1},{-1,0},{0,-1} }; //右、下、左、上
//记录每一个点的坐标和从起点到这个点所用的步数
struct point
{
	int x;
	int y;
	int step;
	point(int i, int j, int k) :x(i), y(j), step(k) {} //使用初始化列表的构造函数
};
stack<point> pts;
int steps = 0;

void clear(stack<point>& q); //定义一个清空队列的函数
void clear(int arr[Length][Length]); //定义一个清空步数缓存的函数

void dfs(point pt)
{
	cout << "  正在点(" << pt.x << ',' << pt.y << ")\n";
	for (int dir = 0; dir < 4; dir++)
	{
		int next_x = pt.x + direction[dir][0];
		int next_y = pt.y + direction[dir][1];
		if (0 <= next_x && next_x < Length && 0 <= next_y && next_y < Length) //判断方向是否有效
		{
			cout << "    正在测试有效点(" << next_x << ',' << next_y << ")高度\n";
			if (Height[next_x][next_y] < Height[pt.x][pt.y]) //判断下一个方向是否高度更低,能够前进
			{
				if (maxsteps[next_x][next_y] < pt.step + 1) //判断如果向该方向再走一步,是否步数比其他已经尝试的方式都大(也就是砍掉不可能是最大的路径)
				{
					pts.push(point(next_x, next_y, pt.step + 1)); //将下一个可行的点存入队列中
					maxsteps[next_x][next_y] = pt.step + 1; //更新步数表格
				}
			}
		}
	}
	steps = pt.step + 1 > steps ? pt.step + 1 : steps;
	return;
}

int main()
{
	int max_steps = 0;
	for (int start_x = 0; start_x < Length; start_x++)
	{
		for (int start_y = 0; start_y < Length; start_y++)
		{
			cout << "正在以(" << start_x << ',' << start_y << ")为起点\n";
			steps = 0;
			clear(maxsteps);
			clear(pts);
			dfs(point(start_x, start_y, 1));
			while (!pts.empty())
			{
				point next_point = pts.top();
				pts.pop();
				dfs(next_point);
			}
			steps--; //因为最后一次无法前进的点还记了一次步数,所以要减掉
			cout << "步数: " << steps << '\n';
			max_steps = steps > max_steps ? steps : max_steps; //计入最大步数
		}
	}
	cout << max_steps;
	return 0;
}

void clear(stack<point>& s)
{
	stack<point> empty;
	swap(empty, s);
}

void clear(int arr[Length][Length])
{
	for (int i = 0; i < Length; i++)
	{
		for (int j = 0; j < Length; j++)
		{
			arr[i][j] = 0;
		}
	}
}

几乎没有做改变,仅仅是用栈替换了队列,来存储将要读取的信息。同样,输出的结果依然是25,不过,从中间的步骤可以看出二者的区别。

我们同样以(2,2)为起点,比较一下二者所探索的点的不同之处:

BFS
正在以(2,2)为起点
正在点(2,2)
正在测试有效点(3,2)高度
正在测试有效点(2,3)高度
正在测试有效点(1,2)高度
正在测试有效点(2,1)高度
正在点(3,2)
正在测试有效点(4,2)高度
正在测试有效点(3,3)高度
正在测试有效点(2,2)高度
正在测试有效点(3,1)高度
正在点(2,3)
正在测试有效点(3,3)高度
正在测试有效点(2,4)高度
正在测试有效点(1,3)高度
正在测试有效点(2,2)高度
正在点(1,2)
正在测试有效点(2,2)高度
正在测试有效点(1,3)高度
正在测试有效点(0,2)高度
正在测试有效点(1,1)高度
正在点(2,1)
正在测试有效点(3,1)高度
正在测试有效点(2,2)高度
正在测试有效点(1,1)高度
正在测试有效点(2,0)高度
正在点(4,2)
正在测试有效点(4,3)高度
正在测试有效点(3,2)高度
正在测试有效点(4,1)高度
正在点(3,3)
正在测试有效点(4,3)高度
正在测试有效点(3,4)高度
正在测试有效点(2,3)高度
正在测试有效点(3,2)高度
正在点(2,4)
正在测试有效点(3,4)高度
正在测试有效点(1,4)高度
正在测试有效点(2,3)高度
正在点(1,3)
正在测试有效点(2,3)高度
正在测试有效点(1,4)高度
正在测试有效点(0,3)高度
正在测试有效点(1,2)高度
……

正如图中,程序先按下、右、上、左的顺序发现四个红箭头都可以前往;然后先按下的红箭头到达队列中的第一个点,判断两个蓝箭头均可前往;接着按右的红箭头到达队列中的第二个点,本来绿箭头应该先到达黄色高亮的点,然而已经判断到了蓝箭头已曾到达过这个点了,并且绿箭头到达黄点也并不更优,于是剪断了这根枝,绿箭头只到达了右边和上面的点。

DFS
正在点(2,2)
正在测试有效点(3,2)高度
正在测试有效点(2,3)高度
正在测试有效点(1,2)高度
正在测试有效点(2,1)高度
正在点(2,1)
正在测试有效点(3,1)高度
正在测试有效点(2,2)高度
正在测试有效点(1,1)高度
正在测试有效点(2,0)高度
正在点(2,0)
正在测试有效点(3,0)高度
正在测试有效点(2,1)高度
正在测试有效点(1,0)高度
正在点(3,0)
正在测试有效点(4,0)高度
正在测试有效点(3,1)高度
正在测试有效点(2,0)高度
正在点(4,0)
正在测试有效点(4,1)高度
正在测试有效点(3,0)高度
正在点(4,1)
正在测试有效点(4,2)高度
正在测试有效点(3,1)高度
正在测试有效点(4,0)高度
正在点(4,2)
正在测试有效点(4,3)高度
正在测试有效点(3,2)高度
正在测试有效点(4,1)高度
正在点(4,3)
正在测试有效点(4,4)高度
正在测试有效点(3,3)高度
正在测试有效点(4,2)高度
正在点(4,4)
正在测试有效点(3,4)高度
正在测试有效点(4,3)高度
……

对于DFS,因为栈后进先出,所以按相反的顺序测试点,即左、上、右、下。判断时,点先按第一条路走到底,也就是红箭头一直走到(0,0)处后,再沿着树回溯。跳过一些步骤举例,例如回溯到黄色高亮点往蓝箭头方向搜索时,蓝箭头本来优先探索左方向,然而已经判断到了红箭头已曾到达过这个点了,并且蓝箭头到达这个点也并不更优,于是同样地剪断了这根枝,蓝箭头只能跳过向左,先向右进行搜索。

结论

动态规划利用存储已经运算过的内容,避免了对某些运算内容的重复运算,从而达到了减少运算量的目的。这是一种优化力度大,通用性强的计算机算法、以至数学方法。

聊聊动态规划之BFS/DFS+剪枝——以滑雪问题为例(上)

首先让我们看看BFS是个什么样的运作原理:

BFS是以广度为优先,也就是先把同级的每一个节点(例如2)先进行运算,同时将和它相邻的需要运算的节点入队,稍后再算(例如2对应的5、6),当同级的所有节点都运算完了的时候(2、3、4),由于队列先进先出的特点,自然轮到了下一级节点(5、6、7、8、9)

然后再看看在BFS中,树是如何剪枝的:

在很多问题中(尤其是棋盘移动之类的),会出现多个节点连上同一个节点的情况(如图中的2、3、4都连着下一级的6),这时候根据条件判断哪一条更优。例如图中红色数值越大越优,当判断到2连6的数字是20时,6的红色值被赋予20,与2相连;当判断到3连6的数字是30时,30比20大,6的红色值被赋予30,与3相连;当判断到4连6的数字是10时,10比30小,6的红色值不变,仍然与3相连;这样,就剪断了2和6,4和6的枝。

那么,没有剪枝的话,会是怎样的呢?

不多解释了,来看一个例子,题目粘下来是这样:

Michael喜欢滑雪,这并不奇怪, 因为滑雪的确很刺激。可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道载一个区域中最长底滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子 

 1   2   3  4  5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

一个人可以从某个点滑向上下左右相邻四个点之一,当且仅当高度减小。在上面的例子中,一条可滑行的滑坡为24-17-16-1。当然25-24-23-…-3-2-1更长。事实上,这是最长的一条。

输出最长区域的长度,本例中是 25。

先上代码吧:

#include <iostream>
#include <queue>
using std::queue;
using std::cout;

const int Length = 5;
const int Height[Length][Length] =
{
	{1, 2, 3, 4, 5},
	{16, 17, 18, 19, 6},
	{15, 24, 25, 20, 7},
	{14, 23, 22, 21, 8},
	{13, 12, 11, 10, 9}
};
int maxsteps[Length][Length] = { 0 };

const int direction[4][2] = { {1,0},{0,1},{-1,0},{0,-1} }; //右、下、左、上
//记录每一个点的坐标和从起点到这个点所用的步数
struct point
{
	int x;
	int y;
	int step;
	point(int i, int j, int k):x(i),y(j),step(k) {} //使用初始化列表的构造函数
};
queue<point> pts;
int steps = 0;

void clear(queue<point>& q); //定义一个清空队列的函数
void clear(int arr[Length][Length]); //定义一个清空步数缓存的函数

void bfs(point pt)
{
	cout << "  正在点(" << pt.x << ',' << pt.y << ")\n";
	for (int dir = 0; dir < 4; dir++)
	{
		int next_x = pt.x + direction[dir][0];
		int next_y = pt.y + direction[dir][1];
		if (0 <= next_x && next_x < Length && 0 <= next_y && next_y < Length) //判断方向是否有效
		{
			cout << "    正在测试有效点(" << next_x << ',' << next_y << ")高度\n";
			if (Height[next_x][next_y] < Height[pt.x][pt.y]) //判断下一个方向是否高度更低,能够前进
			{
				if (maxsteps[next_x][next_y] < pt.step + 1) //判断如果向该方向再走一步,是否步数比其他已经尝试的方式都大(也就是砍掉不可能是最大的路径)
				{
					pts.push(point(next_x, next_y, pt.step + 1)); //将下一个可行的点存入队列中
					maxsteps[next_x][next_y] = pt.step + 1; //更新步数表格
				}
			}
		}
	}
	steps = pt.step + 1 > steps ? pt.step + 1 : steps;
	return;
}

int main()
{
	int max_steps = 0;
	for (int start_x = 0; start_x < Length; start_x++)
	{
		for (int start_y = 0; start_y < Length; start_y++)
		{
			cout << "正在以(" << start_x << ',' << start_y << ")为起点\n";
			steps = 0;
			clear(maxsteps);
			clear(pts);
			bfs(point(start_x, start_y, 1));
			while (!pts.empty())
			{
				point next_point = pts.front();
				pts.pop();
				bfs(next_point);
			}
			steps--; //因为最后一次无法前进的点还记了一次步数,所以要减掉
			cout << "步数: " << steps << '\n';
			max_steps = steps > max_steps ? steps : max_steps; //计入最大步数
		}
	}
	cout << max_steps;
	return 0;
}

void clear(queue<point>& q) 
{
	queue<point> empty;
	swap(empty, q);
}

void clear(int arr[Length][Length])
{
	for (int i = 0; i < Length; i++)
	{
		for (int j = 0; j < Length; j++)
		{
			arr[i][j] = 0;
		}
	}
}

首先定义好高度的二维数组,以及用来存到某一点所需步数的临时二维数组;为了代码简洁,同时定义好含四个方向的数组,以及包含着x、y坐标和已经移动的步数的point结构(并且写好构造函数)

主要思路是这样的:

遍历所有的起点
–清空临时数据
–检测起点的可移动信息,并将可以去的下一个点入队(函数bfs())
–队列非空时,对每个元素再检测可移动信息,并将下一个点入队(函数bfs())
–计入每个起点最终的最大移动步数

函数bfs的思路:

遍历四个方向的下一个点进行测试
–判断该方向下一个点是否还在二维数组中
—-判断下一个点的高度是否比当前点更小
——判断下一个点是否没有别的路径到达过,或者有但是路径比正在测试的路径短(注意:这里正是树剪枝的办法,直接剪掉不可能是最长的路径)
——–如果均为是,将下一个可以到达的点入队,并且更新步数的表格
遍历完之后,总步数+1(因为最后一次无路可走也会导致步数+1,最终结果要-1)

样例输出:

首先,最终结果输出的是25,也就是从(2,2)点的“25”开始出发的最长路径。

然后以图上的(4,4),既“9”为原点为例,可以看出探索的路径:
最初在起点(4,4),判断到下一个存在的点有(3,4)和(4,3),挑选出没有经过过、并且高度更低的点(3,4),再判断存在的点(4,4),(2,4)和(3,3)。因为高度只能下降,可以没有必要去把曾经经过的点标记为已经过。通过多次测试,最终按照9、8、7、6、5、4、3、2、1的顺序找到了步数为9的最大路径。

以下是完整的输出结果: