20250830算法题解析-偏爱的字符串

废话

最近博客突然打不开了,看了一眼是PHP开始报莫名奇妙的错误。这个博客主题是当年随便从网上找的一个,历经多个PHP和wordpress大版本变化还凑合能正常展示已经非常不易了,中途历经多次魔改和打补丁。原计划是在10周年之前把所有文章彻底重写一次换掉wordpress,但以我这个鸽子的风格看了一眼还有1个月时间显然不现实,所以这次我直接放弃了,直接用官方的模板改了改css就成了现在这个样子。

看了一眼历史记录,我的第一篇博客还是在大学时候联系ubuntu linux搭建wodpress后写的,当时正好是大一的期末即将C语言考试,写了一系列的C语言教程当作期末复习。众所周知我的算法比较弱(相比于栋神或然妞),并且从未在算法上怎么努力过,就这么得过且过了10年,做深度算法的分析屈指可数。印象中比较清晰的只有KMP算法、Levenshtein距离、上班时为了优化聊天室pin消息new红点的算法、为了模仿outlook日程布局设计的排版算法。

前段时间在群里看到有人在讨论一个算法题,我简单分享了思路,但懒得动手。这次正好做个深度解析。顺带一提虽然现在ai编程非常的火,工作的话因为讲究效率优先有时候无需考虑原理,只要结果对就行。但长期来看我的态度还是要磨练下基本功,不然AI错了你根本纠正不回来,所以本次不会使用任何AI补全,完全一步步手动实现再逐步优化。

原题目

https://www.nowcoder.com/practice/294da7ea5f184655a31bf659035fc874?tab=note

视频版

https://www.bilibili.com/video/BV1cChqzqEh8

描述

小李天生偏爱一些字符,对于一个字符串,他总是想把字符串中的字符变成他偏爱的那些字符。如果字符串中某个字符不是他所偏爱的字符,称为非偏爱字符,那么他会将该非偏爱字符替换为字符串中距离该字符最近的一个偏爱的字符。这里的距离定义即为字符在字符串中的对应下标之差的绝对值。如果有不止一个偏爱的字符距离非偏爱字符最近,那么小李会选择最左边的那个偏爱字符来替换该非偏爱字符,这样就保证了替换后的字符串是唯一的。小李的所有替换操作是同时进行的。
假定小李有 m 个偏爱的字符,依次为c1,c2…cm,当小李看到一个长度为 n 的字符串 s 时,请你输出小李在进行全部替换操作后形成的字符串。

输入描述:

第一行输入两个正整数n,m。
接下来一行输入m个字符c1,c2…cm,每两个字符之间用空格隔开,表示小李偏爱的字符。

接下来一行输入一个字符串s。

1≤n≤1e5,1≤m≤26,保证题目中所有的字符均为大写字符,小李偏爱的字符互不相同,且偏爱字符至少出现一次。

输出描述:

输出一行字符串,表示小李将给定的字符串s替换后形成的字符串。

输入:

输出:

说明:

题目分析

我做算法题因为没有限时,所以一般来说我都是先用暴力手段写正确结果,再逐步优化

题目里废话实在太多,翻译成人话就是把后续所有输入都改成指定的几个字母,以最近的为准,如果距离一样用左边的。

暴力版本

加一点优化

根据刚才的运行结果,如果尝试打印寻找过程

注意到每次左侧寻找的距离都是递增的,so优化思路就是不要每次都从头开始搜,如果还没出现新的偏爱字符,那上一次寻找的结果直接+1就是本次搜索的结果,如果距离右侧最新的偏爱字符过半了,那就是右侧的结果。因此之前循环的状态需要记录下左侧和右侧的距离或索引。

TODO 待编写代码

再加一点优化

按上面的思路,如果能提前知道哪些位置是偏爱字符,那么根本不需要逐个字符判断、搜索、替换,可以直接按偏爱字符的位置就能计算最终的结果。你会发现最终结果只和偏爱字符的位置有关,和其他输入的字符无关,你甚至都不需要读入其他字符也能计算出最终结果

TODO 待编写代码

另类思路

按上面所说既然结果和其他字符无关,只需要特殊字符和对应的位置+总字符串长度就能计算的话。实时处理也是可能的,这也是目前AI输入输出常见的表现形式,打字机效果。当输入一个字符后,实时输出最终结果,当发现前几个字符结果不对了,删除前n个字符后纠正回正确的字符。这种算法不需要预处理,但因为无法预知后续输入所以必须有纠正的过程出现。实际实现逻辑只需记录最后一次输入的偏爱字符和当前字符和最后一次输入的偏爱字符距离,即可知道是应该打印最后一次的偏爱字符,还是应该删除前n个距离当前输入的偏爱字符更近的字符。

TODO 待编写代码

20210424 Levenshtein距离解析

0.前言

距离上一次分析算法已经过去好几年了- -以我的算法水平其实是很讨厌分析算法的,一个是想写明白耗时很长,一个是说人话很啰嗦。但是写代码或公式又大部分人很难理解。今天要分析的算法是字符串相似度,Levenshtein距离

形式化描述:给定字符串strA,strB,求解dist(strA,strB)的值,dist为Levenshtein距离

说人话:两个字符串,编辑几次能变得完全一样,编辑允许的操作(插入,删除,替换字符)

0.定理/公理,人人都能看懂的情况

字符串基本操作:

连接 下文用+代表

删除字符 下文用 删除strA[0]表示

增加字符 下文用 str[x]=’a’表示 x从0开始

替换字符 下文用 str[x]=’a’表示 x从0开始

(这里一切的基础,类似于公理无需证明、显而易见、或可以轻易穷举证明的规律或法则)

0.1 (多嘴说两句,空串””可以理解为代数系统幺元的概念)

初始状态strA=”” strB=”abcdefgxxxxxx”

显然dist(strA,strB)=len(strB) len代表strB的长度

0.2 交换律

显然交换律成立

strA+strB == strB + strA

dist(strA,strB) == dist(strB,strA)

0.3 结合律(瞎起的名,可能不够严谨)

dist(strA,strB) == dist(strA+strC,strB+strC) == dist(strC+strA, strC+strB)

初始状态strA=”xxxxa…” strB=”xxxxb…”

显然,如果前缀x的部分相等,不论x多长,都可以等价于直接去掉,变成strA=”a” strB=”b”

初始状态strA=”axxxx” strB=”bxxxx”

显然,如果后缀x的部分相等,不论x多长,都可以等价于直接去掉,变成strA=”a” strB=”b”

特殊情况:初始状态strA=”a” strB=”a”

显然,当strA和strB完全相等时:dist(strA,strB)==dist(“”,””)==dist(“”,len(“”))==0

1.下面从最简单的情况开始分析

从最简单的情况开始分析,只有一个字母的时候,有这么几种情况

1.1.case1

初始状态strA=”a” strB=””,显然有这么几种情况

1.1步=删除1次

1.1.删除strA[0]->””,””

2.1步=插入1次

2.1.插入strB[0]=’a’,->”a”,”a”

1.2.case2

初始状态strA=”” strB=”b”,显然有这么几种情况

1.1步=删除1次

1.1.删除strB[0]->””,””

2.1步=插入1次

2.1.插入strA[0]=’b’,->”b”,”b”

2.稍微复杂一点,两个不等字符跟别构成的字符串

初始状态strA=”a” strB=”b”

以下表示格式为strA,strB

1.

“a”,”b”

1.1.替换strB[0]=’a’->”a”,”a”

共1步=替换1次 => dist(“”, “”) + 1

如何理解dist(“”,””)是这个地方的关键,在替换的策略下,总次数实际是是替换位置前面所有的部分的编辑次数,加上替换的这1次即是整个字符串的总次数,这个case下位置0前面没东西了,所以是空串””

2.

“a”,”b”

2.1.替换strA[0]=’b’->”b”,”b”

共1步=替换1次 => dist(“”, “”) + 1

同上:如何理解dist(“”,””)是这个地方的关键,在替换的策略下,总次数实际是是替换位置前面所有的部分的编辑次数(不包括替换的位置自己),加上替换的这1次即是整个字符串的总编辑次数,这个case下位置0前面没东西了,所以是空串””

3

“a”,”b”

3.1.插入strA[0]=’b’->”ba”,”b”

3.2.插入strB[1]=’a’->”ba”,”ba”

共2步=插入1次+ dist(“ba”,”b”)== 插入1次 + dist(“a”,””)  =>  dist(“a”,””) + 1

如何理解 dist(“a”,””)是这个地方的关键:

实际上原本是dist(“ba”,”b”),因为前缀相同,所以变成了dist(“a”,””)

在这种插入策略下,插入位置之前的部分总是一样的(包括插入位置自己),所以总次数实际是是插入位置后面所有的部分的编辑次数(不包括插入的位置自己),加上插入的这1次即是整个字符串的总次数

这个case下strA[0]位置后面是”a”,strB后面是””

4

“a”,”b”

4.1.插入strB[0]=’a’->”a”,”ab”

4.2.插入strA[1]=’b’->”ab”,”ab”

共2步=插入1次+ dist(“a”,”ab”)== 插入1次 + dist(“”,”b”)  =>  dist(“”,”b”) + 1

如何理解 dist(“”,”b”) 是这个地方的关键:

实际上原本是dist(“a”,”ab”),因为前缀相同,所以变成了dist(“”,”b”)

在这种插入策略下,插入位置之前的部分总是一样的(包括插入位置自己),所以总次数实际是是插入位置后面所有的部分的编辑次数(不包括插入的位置自己),加上插入的这1次即是整个字符串的总次数

这个case下strA[0]位置后面是””,strB后面是”b”

5

“a”,”b”

共2步 显然等价于1

5.1.删除strB[0]->”a”,””

5.2.插入strB[0]=’a’->”a”,”a”

6

“a”,”b”

共2步 显然等价于2

6.1.删除strA[0]->””,”b”

6.2.插入strA[0]=’b’->”b”,”b”

7

“a”,”b”

7.1.插入strB[1]=’a’->”a”,”ba”

7.2.插入strA[0]=’b’->”ba”,”ba”

共2步=插入1次+ dist(“a”,”ba”)== 插入1次 + dist(“”,”b”)  =>  dist(“”,”b”) + 1  同4

如何理解 dist(“”,”b”)是这个地方的关键:

实际上原本是dist(“a”,”ba”),因为后缀相同,所以变成了dist(“”,”b”)

在这种插入策略下,插入位置之后的部分总是一样的(包括插入位置自己),所以总次数实际是是插入位置前面所有的部分的编辑次数(不包括插入的位置自己),加上插入的这1次即是整个字符串的总次数

这个case下strA[0]位置前面是””,strB[1]前面是”b”

8.

“a”,”b”

8.1.插入strA[1]=’b’->”ab”,”b”

8.2.插入strB[0]=’a’->”ab”,”ab”

共2步=插入1次+ dist(“ab”,”b”)== 插入1次 + dist(“a”,””)  =>  dist(“a”,””) + 1  同3

如何理解 dist(“a”,””) 是这个地方的关键:

实际上原本是dist(“ab”,”b”),因为后缀相同,所以变成了dist(“a”,””)

在这种插入策略下,插入位置之后的部分总是一样的(包括插入位置自己),所以总次数实际是是插入位置前面所有的部分的编辑次数(不包括插入的位置自己),加上插入的这1次即是整个字符串的总次数

这个case下strA[1]位置前面是”a”,strB[0]前面是””

 

综上1278,不难发现,最小的次数是1278中的最小值(选1234就是倒着了,一样的,两个方向都可以的)

dist(“a”,”b”) == min(dist(“”, “”) + 1, dist(“a”, “”) + 1, dist(“”, “b”) + 1)

或写做min(dist(“”, “”) , dist(“a”, “”) , dist(“”, “b”) ) + 1

认真思考下,替换策略计算的结果为替换位置之前的部分,插入策略因位置不同但相互对称,所以刚好是去掉插入位置的字符前面的部分

所以dist(“a”,”b”) == 1

3.再复杂一些

根据2的结论,能否扩充到长度为n的情况呢?

dist(“abcdef”,”agchef”)

根据定理3 ==> dist(“abcd”, “agch”)

根据定理3 ==> dist(“bcd”, “gch”)

根据规律 ==> min(    dist(“bc”, “gc”),    dist(“bcd”, “gc”),    dist(“bc”, “gch”) ) + 1

根据规律 ==>min(

dist(“b”, “g”),

min(  dist(“bc”, “g”),     dist(“bcd”, “g”),      dist(“bc”, “gc”)     ) + 1 ,

min( dist(“b”, “gc”),     dist(“bc”, “gc”),       dist(“b”, “gch”)   )  + 1

) + 1

根据规律 ==>min(

dist(“b”, “g”),

min(

min(dist(“b”, “”),dist(“bc”, “”),dist(“b”, “g”))+1,

min(dist(“bc”,””), dist(“bcd”,””), dist(“bc”,”g”))+ 1

dist(“b”,”g”)

) + 1,

min(dist(“b”, “gc”),  dist(“bc”, “gc”),  dist(“b”, “gch”)) + 1

) + 1

根据规律 ==>min(

dist(“b”, “g”),

min(

min(dist(“b”, “”),dist(“bc”, “”),dist(“b”, “g”))+1,

min(dist(“bc”,””), dist(“bcd”,””), min(dist(“b”,””), dist(“bc”,”g”), dist(“b”,”g”)) + 1) +  1

dist(“b”,”g”)

) + 1,

min(dist(“b”, “gc”),  dist(“bc”, “gc”),  min(dist(“”, “gc”), dist(“b”, “gc”), dist(“”, “gch”)) + 1) + 1

) + 1

根据规律 ==>min(

1,

min(

min(1,2,1)+1,

min(2,3, min(1,2,1) + 1) +  1

1

) + 1,

min(2, 1,  min(2, 2, 3) + 1) + 1

) + 1

根据规律 ==>2

4.程序实现

次数统计后如下,显然部分内容被重复计算了

dist(“”,”g”)
dist(“”,”gc”) * 2
dist(“”,”gch”)
dist(“b”,””) * 3
dist(“b”,”g”) * 5
dist(“b”,”gc”)
dist(“bc”,””) * 3
dist(“bc”,”gc”)
dist(“bcd”,””)

直接加缓存,或按状态转移方程填表即可

参考资料

https://zhuanlan.zhihu.com/p/91645988

CAS对接的坑,一个字母引发的惨案

有两个类定义如下

管理后台生产的JSON配置是 @class: org.apereo.cas.ws.idp.services.WsFederationClaimsReleasePolicy

后台反序列化提示WsFederationClaimsReleasePolicy不是RegisteredServiceAttributeReleasePolicy的子类

MD 找了我一天,最后就改了一个字母,给官方发了pullrequest

js写的类名是Ws,后台JAVA写的是WS,已经给官方提了PR

https://github.com/apereo/cas-management/pull/160/files

调试方法总结

 

简介

可能是目前为止最全的调试指南

前言

在编程开发的过程中总会遇到一些迷人的BUG,最近几个人来找我帮忙DEBUG,都是很难通过搜索引擎解决的问题。最后发现都是很神奇的错误,于是决定写一篇文章好好来聊一聊如何DEBUG。

0 整体思路

总的来说,问题的本质是预期结果A与实际结果B不符。这个时候就要DEBUG了,下文将以本人DEBUG的经验说一说DEBUG的原则,以及从不同的平台来聊一聊如何DEBUG。

调试条件:
– 有预期结果
– 结果可观测
(注意一些薛定谔的结果,试图观测可能会破坏原有状态)
基本方法:
– 控制变量
– 二分法查找

所以只要让结果可观测,那么就可以借此定位出现问题的部分在哪。一般来说都是打印日志或者IDE断点

0.1 分解问题

当遇到BUG的时候,从结果出发分析结果出现的过程,追溯到输入。一般来说,都是输入经过n个处理流程后得到最后出现的结果。

0.2 定位问题

从结果开始追溯,观测每个阶段结果,从而确定从哪个流程开始结果开始不对的。

0.3 设计方案

设法观测中间结果,使结果可观测。
设法改变出问题阶段的输出恒为正确答案,观测最终结果是否正确。
如果正确,说明该流程后面的流程都没问题,继续向前定位。
如果不正确,说明后面的流程至少有一个有问题,继续向后定位。

1 Web端调试

常用工具为Chrome的F12的开发人员调试工具。
APP上的内嵌网页如果需要依赖APP则使用Remove device功能
一般网页的加载过程:浏览器接收HTTP报文,解析HTML并渲染,加载JS、CSS等静态资源,解析JS脚本并执行。

1.1 html

HTML最终结果的调试在elements标签中可以动态更改,查看效果。
前后端分离的项目大多是JS来渲染DOM并填充数据,所以DOM是动态生成的,需要查看原始的source,而不是elements

1.2 js

JS在source中可以寻找源码,添加断点,单步调试。

1.3 css

CSS的调试在style窗口可以动态修改样式。

2.移动端调试(Android/iOS)

移动端的调试大多依赖对应的IDE,以Android为例,Android Studio支持真机下断点进行调试,logcat可以输出调试日志。当然也有一些手机上的sqlite可视化工具可以使用。
也可以改写代码将中间结果图形化直接显示到界面上,或者弹出提示窗进行显示。

3 API调试

API大部分都是基于HTTP协议的,只要抓住HTTP报文,一般问题都可以解决。
API调试可以借助Chrome开发工具的Network标签,可以详细显示HTTP协议的header和body。自带json格式化。拦截对应的HTTP请求,分析请求过程。
如果Chrome开发工具搞不定,可以使用Postman模拟构造HTTP报文来发送请求,当然也可以使用curl生成自定义的HTTP报文。
如果上面的工具依然难以调试,则可以选择设置流量代理或者借助其他软件拦截所有网络流量包分析过程。

4 Server端调试

服务端调试大部分情况下依赖于日志
tail -f filename.log可以持续查看一个文件的变化,用于监控日志非常方便。

4.1 接入层调试

nginx apache有对应的请求日志和加载日志,直接对应查看即可。
如果转发有误则可以考虑放测试文件到对应的文件夹下来测试转发。

4.2 应用层调试

不同的应用服务器也有自己的启动日志和运行日志,可供调试参考。
如果在开发环境下,可以直接借助IDE进行断点调试,追踪问题。如果在特殊环境无法加载IDE,则最简单的办法依然是打印详细日志来追踪程序执行过程,缩小并确定问题范围。
注意未知因素的影响,例如有些框架取数据是自动拼接方法名并调用来取值,注意避开这些自动生成的方法名以免冲突。

5 Linux调试

Linux下的调试大部分都没有图形界面,只能看字符输出,所以多打印中间结果总能找到出问题的部分。

5.1 shell脚本调试

利用echo打印中间结果或使用sh -x参数进行调试。

5.2 shell命令调试

shell命令是在环境变量PATH中搜索的,一般使用which可以查看到命令的可执行文件所在位置,并继续跟踪执行过程即可。

5.3 网络调试

网络出现问题时候可以使用ping和traceroute来追中出问题的网络节点,使用netstat -ano查看端口占用情况。

6 特定语言调试

其实也没什么特殊的,除了借助IDE的断点调试之外,各个语言都有自带的打印函数可以打印中间结果。

6.1 C/C++

printf cout

6.2 Java

System.out.println
Log4j框架

6.3 Python

print
logging和traceback模块

7 常见错误

7.1 环境错误

开发环境本身配置有误,没有完成测试就继续开发了,避免方法是提前做好环境测试。运行测试的DEMO

7.2 语法错误

IDE一般会提示的,不解释

7.3 拼写错误

手残不解释

7.4 逻辑错误

这类错误是最常见的,也最难调试。也就是所有语法都对,但是结果不对,说明代码逻辑有问题,解决方法就是上面所说的逐步缩小范围,直到一行代码。

7.5 调试的错误

调试本身的错误,由于某些情况下的薛定谔效应无法直接观测结果,只能间接观察的时候容易引起人的误判。想办法避免直接观测即可。

7.6 其他错误

程序都对,但是某配置文件有错,比如makefile文件只能用tab。yaml中只能用空格。python中tab和空格的混用。某些print不支持中文等。

 

spring jmstemplate的坑

jmstemplate 发送文本消息的写法
第一种手动构造msg的话不需要做特殊处理,但是很麻烦

调用方写法

第二种自动构造msg需要在注入bean的时候指定msgConverter,写法简洁但是需要注意不要做第手动转换

调用方写法

MongoDB基础

https://www.mongodb.com/presentations/webinar-back-to-basics-thinking-in-documents?utm_campaign=T5_V3_DEV_ZH_E1_Schema_Design_A&utm_medium=email&utm_source=Eloqua

https://www.slideshare.net/mongodb/webinar-back-to-basics-thinking-in-documents

[slideshare id=52598563&doc=thinkingindocuments1-150909190439-lva1-app6891]

apache反向代理tomcat获取用户真实IP

校内CAS单点登录的审计日志中,用户的IP地址一直是服务器地址。这个问题拖了好久,今天终于解决了。
通过追查日志的来源,发现是自己实现了一个appender配置在了log4j中。
从这个类入手开始追查代码,最终发现IP的来源是通过request.getRemoteAddr这个方法来获得的。
而这个方法是来源于tomcat自身的servlet-api提供,返回的向tomcat发起请求的客户端IP。

但是一般情况下tomcat都会用nginx或apache代理tomcat,所以这个方法得到的IP就是服务器自身的IP而不是真实的远程IP了
而我又不想更改CAS的源码重新部署,所以只能想办法更改tomcat,让tomcat得到真实的IP。
根据apache文档所述,proxy模块会自动添加这三个header。
但是还差两个header没有添加,于是引入header模块使用下面的指令设置剩下的信息。
RequestHeader set x-forwarded-by “IP”
RequestHeader set x-forwarded-proto “https”
由于我的服务器是https协议,所以写了https。

根据tomcat文档所述,在tomcat端的server.xml中
Engine级别下添加Value节点。

文档写的很清楚internalProxies指定的正则所匹配的地址不会在x-forwarded-by中出现,而trustedProxied正则匹配的地址会出现在x-forwarded-by中。
我想要的结果是真实IP为用户IP,同时添加校内服务器的ip地址进入x-forwarded-by中,所以理论上我值需要把服务器IP地址段的正则写入trustedProxied即可。但是实际测试结果依然不对。
getRemoteAddr方法返回的依然是服务器的IP而不是真实IP。

而如果我在internalProxies服务器IP地址段正则的话,getRemoteAddr返回的结果就是正常的,但是x-forwarded-by字段就看不见代理服务器的IP了。

同时更改tomcat日志的格式如下。