基于动态规划算法的短信模板推导功能

需求背景

在我司负责的其中一个微服务为公司的各个事业线提供了整个短信接口。
受限于日益抓紧的电信运营商的政策,短信发送越来越困难。
各个短信服务商都提出了类似的报备短信模板的要求,否则短信发送速率会受影响。
一般而言,各个系统发送新短信的时候会向短信服务商做增量报备。
但是一旦更换服务商,就需要整理所有短信模板了。

初版

人工整理过往短信模板,需要多事业线人员参与,耗费人力成本很大,而且容易漏。
维护短信模板文档,可以作为以后的方案,但是首次使用仍然需要人工整理过往短信模板,避不开工作量。
故考虑通过收集近期1~3个月短信自动分析短信模板。

经研究,php提供了similar_text()计算文本相似度,单条短信时间复杂度O(n^3)
于是我们可以通过设置合理的阈值,遍历短信记录和生成的示例,生成短信示例。
整个脚本的时间复杂度是O(m.n) m:短信记录数 n:短信模板数目

改进

平均模板数目有200个,因种种原因我们更换过6次短信服务商。

使用上述方案,每次集中报备都需要手动扣出非模板的动态内容,效率不高。

故需要有模板分析工具。

其中由于短信模板中的动态内容内容常为名字等普通格式的内容,无法使用正则等传统方式分析,也没找到相关的类库,工具或者方法。

固考虑通过将相似度高的同类字符串进行比对,不断提取公共字串来逼近短信模板。

对于两个长度均为n的字符串,按常规双指针遍历方法统计最长的公共字符串,经线下环境测试,在短信长度大于30字时,已无法在正常时间内完成计算两个短信的计算,即无法用于任何长营销短信的短信分析。

因为实际上两条短信计算公共字符串所需要的时间复杂度为O(2^n) n短信平均长度,在n大于20的时候已经无法作为小常数系数忽略。

即整个脚本的时间复杂度暴增为O(m.n.2^l) m:短信记录数 n:短信模板数目 l:平均短信长度

思路

下续为方便说明,定义
短信A为字符串A:其元素为a1,a2.....an..aN
短信B为字符串B:其元素为b1,b2.....bm..bM
a1~anb1~bm对应的公共字符串叫c(m,n)

O(2^n+m)其中大量时间用于计算重叠子问题a1,a2.....an..aN,b1,b2.....bm..bM的各自子集的公共字串,故只要可以分离出重复低次项,即可使用dp的思路降低时间复杂度。

任取短信A的一子串a1~an,与B的子串b1~bm
显而易见:

  1. 若an=bm 则a1~an,b1~bm的的最长子串 c(n,m)为c(n-1,m-1)加上最后的an

  2. 若an!=bm 则 an/bm与最长字串无关,且c(n,m)等价于,c(n-1,m),c(m-1,n)中长度较长者

即实际上在已知an与bm的值的情况下,只需要事先再知道c(n-1,m-1),c(n-1,m),c(n,m-1)三个中间值的信息,即可直接计算出c(n,m),即c(n,m)可在最多n+m次降次后,即可直接,无需完整遍历两个字符串,重复的计算成本被剔除。

为此我们保存一个N*M长度的矩阵用于保存c(n,m)所有中间值的长度。
此外,为了还原子字符串的生长方向需要记录每个c(n,m)是由c(n-1,m-1),c(n-1,m),c(n,m-1)哪一个计算而来

基于动态规划算法的短信模板推导.png

矩阵
从上述矩阵即可知道,两个公共字符串即为*好陈*牛,这种算法的时间从O(2^(M+N))优化到O(M*N) M:字符串A长度,N字符串B长度
宏观上,整个脚本的时间复杂度回归到O(m.n) m:短信记录数 n:短信模板数目。

脚本在线上灰度执行和全量执行都没有问题目标达成。

附源码实现(PHP)

<?php
/**
 * 根据两个字符串,推断公共的模板
 * @author Jiankang maijiankang@foxmail.com
 * Class FindStringTemplate
 */
class FindStringTemplate{

    
    const MARK_LEFT=1;
    const MARK_UP=2;
    const MARK_LEFT_UP=3;

    /**
     * 最长公共子字符串衍生方向矩阵
     * @var array
     */
     private $matrixDirection = [[]];
    /**
     * 最长公共子字符串长度矩阵
     * @var array
     */
     private $matrixLen = [[]];
     
     
     private $str;
     private $str2;
     private $len1;
     private $len2;
     private $par='*';

     private  $strTmpl='';
     private $link = false;
     
     /**
      *
      * FindStringTemplate constructor.
      * @param string  $str1 字符串1
      * @param string $str2 字符串2
      * @param string $par 动态部分使用什么代替
      */
     public function __construct($str1, $str2,$par='*'){
         //目前的执行速度够快了,如果后续短信进一步增加,可以使用SplFixedArray优化常量系数
         $str1 = isset($str1)?$str1:'';;
         $str2 = isset($str2)?$str2:'';;
         $this->str=preg_split('//u', $str1, -1, PREG_SPLIT_NO_EMPTY);
         $this->str2=preg_split('//u', $str2, -1, PREG_SPLIT_NO_EMPTY);
         $this->len1 = count($this->str);
         $this->len2 = count($this->str2);
         $this->par=$par;
     }

     
     
    public function getTempl(){
        $this->buildMatrix();
        if($this->matrixDirection[0][0]!=self::MARK_LEFT_UP){
            $this->strTmpl =$this->par;
            $this->link=true;    
        }else{
            $this->strTmpl ='';
            $this->link=false;
        }
        $this->buildTempl($this->matrixDirection , $this->len1-1, $this->len2-1);
        //$this->strTmpl=preg_replace("|\\{$this->par}+|",$intPar,$this->strTmpl);
        return $this->strTmpl;
    }

    /**
     * 方向矩阵构建
     * @author Jiankang maijiankang@foxmail.com
     */
    private function buildMatrix(){
        $str1=$this->str  ;
        $str2=$this->str2 ;
    
        for($i = 0; $i < $this->len1; $i++){
            for($j = 0; $j < $this->len2; $j++) {
                $this->matrixLen[$i][$j] = 0;
            }
        }
        
        for($i = 0; $i < $this->len1; $i++) {
            for($j = 0; $j < $this->len2; $j++) {
                if($i == 0 || $j == 0) {
                    if($str1[$i] == $str2[$j])
                    {
                        $this->matrixLen[$i][$j] = 1;
                        $this->matrixDirection[$i][$j] = self::MARK_LEFT_UP;
                    }else if($i > 0) {
                        $this->matrixLen[$i][$j] = $this->matrixLen[$i-1][$j];
                        $this->matrixDirection[$i][$j] = self::MARK_UP;
                    }else if($j > 0) {
                        $this->matrixLen[$i][$j] = $this->matrixLen[$i][$j-1];
                        $this->matrixDirection[$i][$j] = self::MARK_LEFT;
                    }else{
                        $this->matrixDirection[$i][$j] = self::MARK_LEFT;
                    }
                }else if($str1[$i] == $str2[$j]) {
                    $this->matrixLen[$i][$j] = $this->matrixLen[$i-1][$j-1] + 1;
                    $this->matrixDirection[$i][$j] = self::MARK_LEFT_UP;
                }else if($this->matrixLen[$i-1][$j] > $this->matrixLen[$i][$j-1])
                {
                    $this->matrixLen[$i][$j] = $this->matrixLen[$i-1][$j];
                    $this->matrixDirection[$i][$j] = self::MARK_UP;
                }else {
                    $this->matrixLen[$i][$j] = $this->matrixLen[$i][$j-1];
                    $this->matrixDirection[$i][$j] = self::MARK_LEFT;
                }
            }
        }
    }


    /**
     * 根据方向矩阵推导短信模板
     * @param $matrixDirection
     * @param $row
     * @param $col
     * @author Jiankang maijiankang@foxmail.com
     */
    private function buildTempl($matrixDirection, $row, $col){
        $str=$this->str;
        if($this->len1 == 0 || $this->len2 == 0 || !($row < $this->len1 && $col < $this->len2)){
            return;
        }
        if($matrixDirection[$row][$col] == self::MARK_LEFT_UP) {
            if($row > 0 && $col > 0){
                self::buildTempl($matrixDirection, $row-1, $col-1);
            }
            $this->strTmpl.=$str[$row];
            $this->link=false;
        }else if($matrixDirection[$row][$col] == self::MARK_LEFT) {
            if($col > 0){
                self::buildTempl($matrixDirection, $row, $col-1);
            }
            if(!$this->link){
                $this->link=true;
                $this->strTmpl.=$this->par;                
            }
        }else if($matrixDirection[$row][$col] == self::MARK_UP) {
            if($row > 0){
                self::buildTempl($matrixDirection, $row-1, $col);
            }
            if(!$this->link){
                $this->link=true;
                $this->strTmpl.=$this->par;
            }
        }
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容

  • 在C语言中,五种基本数据类型存储空间长度的排列顺序是: A)char B)char=int<=float C)ch...
    夏天再来阅读 3,342评论 0 2
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,381评论 0 5
  • 专业考题类型管理运行工作负责人一般作业考题内容选项A选项B选项C选项D选项E选项F正确答案 变电单选GYSZ本规程...
    小白兔去钓鱼阅读 8,988评论 0 13
  • Lua 5.1 参考手册 by Roberto Ierusalimschy, Luiz Henrique de F...
    苏黎九歌阅读 13,793评论 0 38
  • AIX下的date命令不像Linux下有-d参数,所以获取昨天的日期比较蛋疼。百度搜到的千篇一律都是如下答案: 运...
    wyaoo阅读 6,754评论 1 4