zoukankan      html  css  js  c++  java
  • java实现 蓝桥杯 算法训练 Password Suspects

    问题描述
      在年轻的时候,我们故事中的英雄——国王 Copa——他的私人数据并不是完全安全地隐蔽。对他来说是,这不可接受的。因此,他发明了一种密码,好记又难以破解。后来,他才知道这种密码是一个长度为奇数的回文串。

    Copa 害怕忘记密码,所以他决定把密码写在一张纸上。他发现这样保存密码不安全,于是他决定按下述方法加密密码:他选定一个整数 X ,保证 X 不小于 0 ,且 2X 严格小于串长度。然后他把密码分成 3 段,最前面的 X 个字符为一段,最后面的 X 个字符为一段,剩余的字符为一段。不妨把这三段依次称之为 prefix, suffix, middle 。显然, middle 的长度为一个大于 0 的奇数,且 prefix 、 suffix 的长度相等。他加密后的密码即为 A + prefix + B + middle + C + suffix ,其中 A 、 B 、 C 是三个由 Copa 选定的字符串,且都有可能为空, + 表示字符串相连。

    许多年过去了。Copa 昨天找到了当年写下加密后字符串的那张纸。但是,Copa 把原密码、A、B、C 都忘了。现在,他请你找一个尽量长的密码,使得这个密码有可能被当年的 Copa 发明、加密并写下。
    输入格式
      输入包含一个只含有小写拉丁字母的字符串,长度在 1 到 10^5 之内。
    输出格式
      第一行包含一个整数 k ,表示你找到的原密码分成的 3 个部分中有多少个非空字符串。显然 k in {1, 3} 。接下来 k 行,每行 2 个用空格分开的整数 x_i l_i ,表示这一部分的起始位置和长度。要求输出的 x_i 递增。

    起始位置 x_i 应该在 1 到加密后的字符串长度之间。 l_i 必须是正整数,因为你只要输出非空部分的信息。 middle 的长度必须为奇数。

    如果有多组答案,任意一组即可。提示:你要最大化的是输出的 l_i 的总和,而不是 k 。
    样例输入
    abacaba
    样例输出
    1
    1 7
    样例输入
    axbya
    样例输出
    3
    1 1
    2 1
    5 1
    样例输入
    xabyczba
    样例输出
    3
    2 2
    4 1
    7 2
    数据规模和约定
      对于 10% 的数据: n <= 10

    对于 30% 的数据: n <= 100

    对于 100% 的数据: n <= 100000

    存在 20% 的数据,输出文件第一行为 1 。

    刚开始四十分的思路(下面有AC代码)

    输入一段字符串组成为: A + prefix + B + middle + C + suffix 。现在要求取 prefix + middle + suffix ,由于题目说:他选定一个整数 X ,保证 X 不小于 0 ,且 2X 严格小于串长度。那么X可取0。即当X = 0时,也只有一段——middle。

    现在要求取最长的密码串:

    (1)首先求取原字符串中的最长回文串长度,用maxLen0表示。

    (2)由于suffix在字符串尾部,先将suffix反转,然后使用KMP算法,在原字符串中找到第一次与suffix匹配的字符串,记录suffix长度文len1。然后,求取者两端匹配字符串中间剩余的一段字符串的最大回文串的长度len2。此时,求取的密码长度maxLen = 2*len1 + len2。

    在这种情况下,影响maxLen长度的因素就是初始选择的len1长度,len1长度变化,也会导致len2长度的变化。所以,我这里采用1 <= len1 < 原始字符串一半的方法来枚举,求取其中的最大maxLen。个人感觉就是在这里,才导致运算超时…

    package com.liuzhen.systemExe;
    
    import java.util.ArrayList;
    import java.util.Scanner;
    
    public class Main{
        //找出字符串arrayA[i]到arrayA[j]中的最大回文串,并返回其初始位置和长度(PS:此方法称作中心扩展法)
        public int[] getMaxReverse(char[] arrayA, int i, int j) {
            int[] result = new int[2];
            int max = 1;
            int begin = i+1;    //最大回文串的开始位置,初始化为字符i的位置
            int zero = i;      //保存i的初始值
            for(i = i + 1;i < j;i++) {
                int start = i - 1;
                int end = i + 1;
                int count = 1;
                while(start >= zero && end <= j) {    //对于此种情况,只返回奇数位回文串
                    if(arrayA[start] == arrayA[end]) {
                        start--;
                        end++;
                        count = count + 2;
                    }
                    else
                        break;
                }
                if(count > max) {
                    max = count;
                    begin = i - (count-1)/2 + 1;
                }
            }
            result[0] = begin;   //最大回文串开始位置
            result[1] = max;     //最大回文串长度
        
            return result;
        }
        //此处代码是使用Manacher算法求取最大回文串,但是在蓝桥杯练习系统中评分时,也是运行超时,所以在求最大回文串就采用上面更易理解的方法
    //    public int[] getManacher(char[] arrayA, int i, int j) {
    //        int[] result = new int[2];
    //        ArrayList<Character> listA = new ArrayList<Character>();
    //        for(int y = i;y <= j;y++) {
    //            listA.add('#');
    //            listA.add(arrayA[y]);
    //        }
    //        listA.add('#');
    //        int[] P = new int[listA.size() + 1];
    //        int mx = 0;
    //        int d = 0;
    //        for(int y = 1;y <= listA.size();y++) {
    //            if(mx > y) {
    //                P[y] = (P[2 * d - y] > (mx - y) ? (mx - y) : P[2 * d - y]);
    //            } else {
    //                P[y] = 1;
    //            }
    //            
    //            while(y + P[y] < listA.size() && y - P[y] >= 0 && listA.get(y + P[y]) == listA.get(y - P[y]))  
    //                P[y]++;
    //            
    //            if(mx < y + P[y]) {
    //                mx = y + P[y];
    //                d = y;
    //            }    
    //        }
    //        
    //        int max = 0;
    //        int tempY = 0;
    //        for(int y = 0;y < P.length;y++) {
    //            if(P[y] > max) {
    //                max = P[y];
    //                tempY = y;
    //            }
    //        }
    //        int begin = (tempY - 1)/2;
    //        begin = begin - (max - 1) / 2 + i + 1;
    //        result[0] = begin;
    //        result[1] = max - 1; 
    //        return result;
    //    }
        //使用KMP模式匹配,计算next函数值
        public int[] getNext(char[] arrayB) {
            int[] next = new int[arrayB.length + 1];
            int j = 0;
            for(int i = 1;i < arrayB.length;i++) {
                while(j > 0 && arrayB[i] != arrayB[j]) j = next[j];
                if(arrayB[i] == arrayB[j]) j++;
                next[i+1] = j;
            }
            return next;
        }
        //使用KMP算法,找出字符数组arrayB,在arrayA中第一次出现匹配的位置
        public int getKmpFirstPosition(char[] arrayA, char[] arrayB) {
            int position = -1;
            int j = 0;
            int[] next = getNext(arrayB);
            for(int i = 0;i < arrayA.length;i++) {
                while(j > 0 && arrayA[i] != arrayB[j]) j = next[j];
                if(arrayA[i] == arrayB[j])
                    j++;
                if(j == arrayB.length) {
                    position = i - j + 1;
                    break;
                }
            }
            return position;
        }
        
        public void printResult(String A) {
            //当选定整数X = 0时,输出第一行为1,此时只需在A中直接找到有段最长回文串即可,这时的密码最长
            char[] arrayA = A.toCharArray();
            int[] result0 = getMaxReverse(arrayA, 0, arrayA.length-1);
            int maxLen0 = result0[1];   //获得此时回文串的最大长度
            //当X != 0时,最长密码其尾部一定在输入的字符串尾部,即 suffix不为空
            int j = arrayA.length - 1;
            int maxLen = 0;     //用于计算最长密码,初始化为0
            int position1 = 0;
            int position2 = 0;
            int position3 = 0;
            int len1 = 0;
            int len2 = 0;
             for(int lenS = 1;lenS < arrayA.length/2;lenS++) {
                char[] tempS = new char[lenS];
                for(int a = 0;a < lenS;a++) 
                    tempS[a] = arrayA[j-a];
                if(getKmpFirstPosition(arrayA, tempS) == -1) {
                    continue;
                } 
                int tempPosition1 = getKmpFirstPosition(arrayA, tempS) + 1;
                int endPosition1 = tempPosition1+lenS;
                int startPosition3 = arrayA.length-lenS;
                
                if(startPosition3 <= endPosition1)
                    continue;
        
                int[] result = getMaxReverse(arrayA,tempPosition1+lenS-1,j-lenS);
                int tempLen2 = result[1];
                int tempPosition2 = result[0];
                
                if(lenS * 2 + tempLen2 > maxLen) {
                    position1 = tempPosition1;
                    position2 = tempPosition2;
                    position3 = j - lenS + 2;
                    len1 = lenS;
                    len2 = tempLen2;
                    maxLen = lenS * 2 + tempLen2;
                }
            }
             if(maxLen0 >= maxLen) {  
                 System.out.println("1");
                  System.out.println(result0[0]+" "+result0[1]);
             } else {
                 System.out.println("3");
                 System.out.println(position1+" "+len1);
                 System.out.println(position2+" "+len2);
                 System.out.println(position3+" "+len1);
             }
        }
        
        public static void main(String[] args){
            Main test = new Main(); 
            Scanner in = new Scanner(System.in);
        //    System.out.println("请输入一个字符串:");
            String A = in.nextLine();
            test.printResult(A);
        }
    }
    

    大概思路
    d[u][len][state]表示当前递归到了这样一个状态:
    1.当前密码串已经确定的长度是len
    2.用字典树对已确定的密码串进行匹配,到当前状态时,字典树匹配到的结点是u
    3.如果state的二进制数是00001010B,假设密码子串有5个,则表示当前状态已经出现的密码子串有按输入顺序的第2、4个。
    这里实际是一个状态压缩,将所有密码的可能压缩到了三个参数上。

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.util.concurrent.ConcurrentLinkedQueue;
    
    /**
     * 
     * 这个题不是原题,原题来自<a href="https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=524&page=show_problem&problem=3517">UVALive</a>
     * 方法是使用AC自动机(要理解AC自动机需要先掌握KMP算法和字典树) 以及DP算法
     * 参考实现<a href="https://blog.csdn.net/accelerator_/article/details/38758557">CSDN</a>
     */
    public class Main {
    	
    	public static void main(String[] args) {
    		
    		int n = 0;
    		int m = 0;
    		try {
    			// 读取数据
    			BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    			String[] nm = br.readLine().split(" ");
    			n = Integer.parseInt(nm[0]);
    			m = Integer.parseInt(nm[1]);
    			// 构建AC自动机
    			AC ac = new AC(n, m);
    			for(int i = 0; i < m; i ++) {
    				ac.insert(br.readLine(), i);
    			}
    			br.close();
    			ac.build();
    			long ans = ac.dfs(0, 0, 0);
    			System.out.println(ans);
    			if(ans <= 42)
    				ac.print(0, 0, 0);
    		} catch (IOException e) {
    			e.printStackTrace();
    		}
    	}
    	
    }
    
    class AC{
    
    	int maxWord = 105; // AC自动机最大记录字母数,最大等于m*字符串最大长度=100,但是还有根节点,不妨设105
    	int dicLen  = 26;  // 每一个结点的最大子结点数,即有多少个字母
    	int maxLen  = 30;  // 最大密码长度 这里也是个点,不知道为什么写25会出错
    	int state   = (1<<10) + 5;      // 1 << M,实际上是一个用二进制表示的状态集,“1”位表示这个词已经出现过了
    	long[][][] dp = new long[maxWord][maxLen][state]; // 深度搜索、动态规划的矩阵 设d[u][len][state]表示最后一个是字典树的u结点 已选长度为len,状态为state的剩余种数
    	int[] fail  = new int[maxWord]; // 状态转移表
    	int[] val   = new int[maxWord]; // 用一个二进制化的整数表示当前已经出现了多少个词 全0表示当前没有出现词 全1表示字典中的词全出现了
        int[] out   = new int[dicLen];  // 输出用
    	
    	public int[][] trie = new int[maxWord][dicLen]; // 字典树
    	private int nodeIndex = 1; // 当前的下标 0表示根结点
    	
    	int n;
    	int m;
    	public AC(int n, int m) {
    		this.n = n;
    		this.m = m;
    		for(int i = 0; i < maxWord; i ++) {       // 对数组全部赋-1,-1表示没有搜索过,0表示搜索过但是没有匹配项
    			for(int j = 0; j < maxLen; j ++) {
    				for(int k = 0; k < state; k ++) {
    					dp[i][j][k] = -1;
    				}
    			}
    		}
    	}
    	
    	public void insert(String word, int v) {
    		int pre = 0;
    		for (int i = 0; i < word.length(); i ++) {
    		    int code = toInteger(word.charAt(i));
    		    if (trie[pre][code] == 0) { // 如果字典树中不存在这个结点
    		    	trie[pre][code] = nodeIndex ++;   // 增加一个结点 记录这个值 
    		    }
    		    pre = trie[pre][code];
    		}
    		val[pre] |= (1 << v);  // 标记状态 如果AC自动机经过pre结点,则val[pre]记录的词都出现在了字符串中
    	}
    	
    	/**构建next列表*/
    	public void build() {
    		ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<Integer>();    
    		// java自带的队列就是LinkedList 但LinkedList是线程不安全的 这里必需用线程安全的 因为有递归调用问题
    		
    		// 把root的所有的子结点加入到队列中,并将这些结点的fail指向根结点
    		fail[0] = 0;
    		for (int c = 0; c < dicLen; c ++) {
    		    int u = trie[0][c];
    		    if (u != 0) {
    		    	fail[u] = 0;
    		    	queue.add(u);
    	    	}
    		}
    		
    		while (!queue.isEmpty()) {
    			
    			// 弹出一个结点
    		    int r = queue.poll();
    		    // 遍历下面的每一个结点
    		    for (int c = 0; c < dicLen; c ++) {
    				int u = trie[r][c];
    				if (u == 0) { // 如果字典树中不存在当前遍历的结点
    				    trie[r][c] = trie[fail[r]][c]; // 子结点指向父节点失败结点的对应子结点
    				    continue;
    				}
    				queue.add(u);
    				int v = fail[r];
    				while (v != 0 && trie[v][c] == 0)  // 如果还存着fail结点,并且这个结点的对应子结点却不存在,实际就是访问所有fail链
    					v = fail[v];
    				fail[u] = trie[v][c];              // 要么存在 要么为0
    				val[u] |= val[fail[u]];            // 当前遍历这个结点的状态集等于 当前状态集 并上 失败结点指向的状态集
    		    }
    		}
    	}
    	
    	public long dfs(int now, int len, int st) {
    		if (dp[now][len][st] != -1)  // 如果已经搜索过这个结点,直接返回
    			return dp[now][len][st];
    		dp[now][len][st] = 0;        // 否则,开始搜索这个结点
    		if (len == n) {
    		    if (st == (1 << m) - 1)  // 假设输入的m是3,则1 << m后减1得 000...000111,表示3个词都出现了
    		    	return dp[now][len][st] = 1;
    		    return dp[now][len][st] = 0;
    		}
    		for (int i = 0; i < dicLen; i++)
    			dp[now][len][st] += dfs(trie[now][i], len + 1, st|val[trie[now][i]]);
    		return dp[now][len][st];
    	}
    
    	public void print(int now, int len, int st) {
    		if (len == n) {
    		    for (int i = 0; i < len ; i ++)
    		    	System.out.print((char)(out[i] + 'a'));
    		    System.out.println();
    		    return;
    		}
    		for (int i = 0; i < dicLen; i ++) { // 递归查找结果
    		    if (dp[trie[now][i]][len + 1][st|val[trie[now][i]]] > 0) {
    				out[len] = i;
    				print(trie[now][i], len + 1, st|val[trie[now][i]]);
    		    }
    		}
        }
    	
    	public int toInteger(char c) {
    		return c - 'a';
    	}
    	
    }
    
    
  • 相关阅读:
    【精品软件】小蔡电脑助手 V2.0
    C++ 数据结构与算法(二)线性表之单链表
    C++ 数据结构与算法:冒泡排序及改进算法
    C++ 数据结构与算法(四)线性表之循环链表
    const int *p、int*const p、和 int const *p 详解
    C++ 指针和引用的区别
    C++ 数据结构与算法(三)线性表之双向链表
    C++ 数据结构与算法(一)线性表之顺序表
    C++数据结构与算法:选择排序
    VC++ 获取文件夹大小
  • 原文地址:https://www.cnblogs.com/a1439775520/p/12948014.html
Copyright © 2011-2022 走看看