创新实践部第一次培训---算法入门

文章目录

    • 引言——我们为什么要学算法
    • 常见基础错误
      • 手(shou)误(jian)
      • 浮点数判等
      • 声明变量和使用变量太远
      • 忘记初始化
      • 数组开小了
      • 变量开小了
      • 建议的代码书写方式
    • ACM输入输出
      • ACM错误类型
      • 枚举
        • 简述
      • 应用场合
        • 例题.hrbust 1565
    • 模拟
      • 简述
      • 解题技巧
        • [例题1.poj 1068](http://poj.org/problem?id=1068)
        • 例题2.蛇行数
    • 排序
      • 简述
    • 数学定理和模型
    • 深度优先搜索(DFS)
      • 简述
      • 思路模版
    • 队列
    • 广度优先搜索(BFS)
      • 简述
      • 模版
        • [例题1.hrbust 1012](https://www.luogu.com.cn/problem/T151585)
        • 例题2.走迷宫!
    • 最短路 Dijkstra
      • 模板代码
    • KMP
      • 模板代码
    • Manacher
      • 模板代码
    • 字典树
      • 简述
      • 模板代码
    • dp(动态规划)
      • 基本思路
      • 例题

引言——我们为什么要学算法

​ 从课程角度出发:之后我们会面对《数据结构与算法》的课程考试:你将要掌握 链表、栈和队列、八种常见排序的空间时间复杂度和实现、二叉树(哈希树、哈夫曼树、树的搜索)、图论(三种最短路算法)······

​ 从专业角度出发:基础算法是未来我们进行专业算法学习的基础和思维拓展。

常见基础错误

手(shou)误(jian)

  • 出错特征:程序执行流程出乎意料,结果不正确。
for (int i = 0; i < n; i++) {
     
  if (i == n) 
  	  printf("%d\n", i);
  else 
  	  printf("%d ", i);
}
  • 治疗方法:剁手。多剁两次就记住了。

浮点数判等

  • 出错特征:WA到死。

    double a = 1/3*3;
    double b = 1;
    if (a == b) {
           
      printf("Yes");
    }
    
  • 治疗方法:

    const double eps = 1e-5;
    double a = 1/3*3;
    double b = 1;
    if (abs(a-b) < eps) {
           
      printf("Yes");
    }
    
  • 注意点:eps到底取多少? 一般在1e-5到1e-8之间。有些题目卡eps。(就是莫名其妙的一个wa一个ac)

声明变量和使用变量太远

  • 出错特征:Output Limit Error 或 WA 或 RE 或 TE 或 机器爆炸。

  • 治疗方法:先睡一觉。写出这种代码,你一定是太累了。

  • 治疗方法2:多使用全局变量开大数组

忘记初始化

  • 出错特征:WA
  • 出错样例:比如每次使用vis之前没有清false之类。
  • 治疗方案:
    • 每个变量定义的同时就初始化。
    • 提交代码之前,检查所有定义的变量是否已经初始化。

数组开小了

  • 出错特征:差别不大的会WA或TE。差别大的会RE。

  • 出错样例:眼花手抖导致的数组少个0。

  • 治疗方案:数组开的足够大。

    #include 
    using namespace std;
    
    const int MAXN = 1e5+1;
    int a[MAXN];
    

变量开小了

  • 出错特征:部分样例点AC,但是卡了几个WA。

  • 治疗方案:int的取值范围为-2147483648到±2147483648

    ​ 大概是10个0的样子

建议的代码书写方式

  • 良好的代码风格。包括但不限于
    • 有意义的变量名(起名字真的是个技术,没那麼简单,就能找到,聊得来的伴)(小驼峰命名法)
    • 缩进
    • 大括号的位置(选择一个风格保持统一)
    • 有必要的空格使代码清晰
    • c式的变量声明方式(即等到要用的时候再声明,不要在函数开头声明一堆,然后再用)
  • 防御性编程
    • 声明变量后立即初始化,不管是否必要。

ACM输入输出

NOJ1083

这篇Noj中写的非常明确,大家可以课后对照参考一下各种输入输出。

ACM错误类型

Compilation Error 代码编译错误

Runtime Error 对应内存越界访问和堆栈溢出

Time Limit Exceeded 对应算法的时间复杂度不够优化

Memory Limit Exceeded 程序超过了题目的内存限制

Wrong Answer 程序输出的答案不正确

枚举

简述

顾名思义,枚举便是依次列举出所有可能产生的结果,根据题中的条件对所得的结果进行逐一的判断,过滤掉那些不符合要求的,保留那些符合要求的,也可以称之为暴力算法。

应用场合

在竞赛中,并不是所有问题都可以使用枚举算法来解决(事实上,只有少数),只有当问题的所有解的个数不太多时,并在我们题目中可以接受的时间内得到问题的解,才可以使用枚举。

例题.hrbust 1565

  • 题意:给出一个数字n(n<=1000000)请你计算出除了1和n之外n的因子数的个数。
  • 要求:Time Limit: 1000 MS , Memory Limit: 10240 K
  • 思路:**首先我们通过分析n的范围和时限(1000ms)可以知道这道题可以使用枚举,我们可以通过枚举1到n中的每个整数,并判断该数是否是n的因子,使用一个count变量进行统计,时间复杂度是O(n),代码如下:
int count=0;
for(int i=2;i<n;i++)
    if(n%i==0)
        count++;
printf("%d\n",count);
  • 拓展:此题时限是1000ms,使用O(n)的算法可以过,如果把n的范 围继续扩大呢,O(n)的算法就会超时,那么应该怎么办?我们考虑这样一个规律:首先我们假设n是16,那么n的因子分别是1,2,4,8,16,我们可以得到这样一个规律:如果a是n的因子,那么n/a一定也是n的因子!所以我们可以将枚举的范围缩小到√n

模拟

简述

记得我刚开始的时候,最喜欢做的就是模拟题,为什么?因为模拟题很少会用到算法,事实也是如此。模拟题考验的是我们的代码实现能力,简单的模拟题基本不用想,就是水题,但是比较难的模拟题,需要我们仔细思考,要寻找出一种能够在代码上相对来说比较好实现并且可以解决这道题的数据结构,这考验的是我们对数据结构的掌握和对题意向代码的转化。模拟题很耗时,只要静下心,考虑到题中的所有坑点,一般模拟题都可以AC。

解题技巧

  • 模拟的题型,基本难度不大,关键读懂题意。
  • 赛场上不要着急于去快速的解决模拟题,因为这类题,一般做起来比较耗时。
  • 想做好模拟题,需要有活跃的思维和对数据结构等知识的扎实的掌握,基础很重要!
  • 有一些模拟题是可以通过刷题来锻炼出来解题能力的,不过太难的模拟题不推荐大家浪费太多时间在上面。
  • 有一些模拟题,看似没有什么算法,很简单,但是会卡时间,需要大家想一下如何优化,所以大家不要盲目的去解决模拟题。

例题1.poj 1068

S	(((()()())))
P-sequence	    4 5 6 6 6 6
W-sequence	    1 1 1 4 5 6
  • 题意:对于给出的原括号串,存在两种数字密码串:

1.p序列:第i个位置之前存在p[i]个左括号,其中i为右括号。(从该括号对的右括号开始往左数,直到最前面的左括号数,就是pi的值。)

2.w序列:第i个右括号所在位置,能和在它左边的第w[i]个左括号匹配

对给出的p数字串,求出对应的s串。串长限制均为20。

  • 要求:Time Limit: 1000 MS , Memory Limit: 10000 K
  • 思路:清楚了题意后,这道题并不是很难,直接模拟就行了
#include 
using namespace std;
int a[30];

int main(){
     
    int N;
    cin >> N;
    while(N--){
     
        int n;
        cin >> n;
        a[0] = 0;
        for(int i = 1; i <= n; i++){
     
            cin >> a[i];
            int j = i - 1;
            while(a[i] - a[j] < i - j) j--;
            cout << (i-j) <<' ';
        }
        cout << endl;
    }
    return 0;
}

例题2.蛇行数

蛇行数的格式如下:

1 2 6 7 15 16

3 5 8 14

4 9 13

10 12

11

现在请你输出蛇行数完整的n*n矩阵

如:输入 5

输出:

1 2 6 7 15
3 5 8 14 17
4 9 13 18 26
10 12 19 25 32
11 20 24 33 41
#include 
#include 
using namespace std;
int a[1000][1000];
int dir[4][2]={
     0,1,1,-1,1,0,-1,1};

int main()
{
     
	memset(a,0,sizeof(a));
	int n;
	cin>>n;
	int cnt=1,x=0,y=0;
	a[x][y]=cnt;
	while(cnt<=1000){
     
		x=x+dir[0][0];
		y=y+dir[0][1];
		cnt++;
		a[x][y]=cnt;
		
		while(y>0){
     
			x=x+dir[1][0];
			y=y+dir[1][1];
			cnt++;
			a[x][y]=cnt;
		}

		x=x+dir[2][0];
		y=y+dir[2][1];
		cnt++;
		a[x][y]=cnt;
		
		while(x>0){
     
			x=x+dir[3][0];
			y=y+dir[3][1];
			cnt++;
			a[x][y]=cnt;
		}
	}
	for(int i=0;i<=n;i++){
     
		for(int j=0;j<=n;j++)
			cout<<a[i][j]<<" ";
		cout<<endl;	
	}
	return 0;
}

排序

简述

学习计算机的任何一门语言,都要掌握排序算法,参与竞赛更要掌握排序算法,将来找工作笔试面试的时候都会考到排序算法,所以说排序算法是十分重要的,我们要全面掌握并深入理解。

#include 
#include 
using namespace std;
const int MAXN=1e5+1;
struct student{
     
    int num;
    char name[52];
}

bool cmp1(int a,int b){
     
    return a<b;
}

bool cmp2(student a,student b){
     
	return a.num<b.num;
}

int a[MAXN];
student s[MAXN];
int main()
{
     
    int n;
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>a[i];
    for(int i=0;i<n;i++)
        cin>>s[i].num>>s[i].name;
    sort(a,a+n,cmp1);
    sort(s,s+n,cmp2);
	return 0;
}

数学定理和模型

数学作为计算机的基础学科,具有重要的基础意义,在很多独立问题上或许本身就存在着数学的最优解答,希望大家能多了解数学定理,多拓宽自己的数学学习面。

深度优先搜索(DFS)

简述

深度优先搜索的英文简写是DFS(Depth First Search),属于图论中搜索算法中的一种,它所遵循的搜索策略是尽可能“深”地搜索图。

思路模版

#include
#include
#include
using namespace std;
const int maxn=100;
bool vst[maxn][maxn]; // 访问标记
int map[maxn][maxn]; // 坐标范围
int dir[4][2]= {
     0,1,0,-1,1,0,-1,0}; // 方向向量,(x,y)周围的四个方向
bool CheckEdge(int x,int y) // 边界条件和约束条件的判断
{
     
    if(!vst[x][y] && ...) // 满足条件
        return 1;
    else // 与约束条件冲突
        return 0;
}

void dfs(int x,int y)
{
     
    vst[x][y]=1; // 标记该节点被访问过
    if(map[x][y]==G) // 出现目标态G
    {
     
        ...... // 做相应处理
        return;
    }
    for(int i=0; i<4; i++)
    {
     
        if(CheckEdge(x+dir[i][0],y+dir[i][1])) // 按照规则生成下一个节点
            dfs(x+dir[i][0],y+dir[i][1]);
    }
    return; // 没有下层搜索节点,回溯
}

int main()
{
     
    ......
    return 0;
}

例题1.hrbust 1143

  • 题意:有一个泉眼,由于当地的地势不均匀,有高有低,这个泉眼不断的向外溶出水来,这意味着这里在不久的将来将会一个小湖。水往低处流,凡是比泉眼地势低或者等于的地方都会被水淹没,地势高的地方水不会越过。而且又因为泉水比较弱,当所有地势低的 地方被淹没后,水位将不会上涨,一直定在跟泉眼一样的水位上。 所有的地图都是一个矩形,并按照坐标系分成了一个个小方格,Leyni知道每个方格的具体高度。我们假定当水留到地图边界时,不会留出地图外,现在他想通过这些数据分析出,将来这里将会出现一个多大面积的湖
  • 要求:Time Limit: 1000 MS , Memory Limit: 65536 K
#include 
#include 
using namespace std;

const int MAXN=1001;
int vis[MAXN][MAXN];
int map[MAXN][MAXN];
int dir[4][2]={
     1,0,-1,0,0,1,0,-1};
int ans;
int n,m,p1,p2;

void dfs(int x,int y){
     
	vis[x][y]=1;
	ans++;
	for(int k=0;k<4;k++){
     
		int tx=x+dir[k][0],ty=y+dir[k][1];
		if (tx<=n && ty<=m && tx>0 && ty>0 && map[tx][ty]<=map[p1][p2] && !vis[tx][ty]){
     
			dfs(tx,ty);
		}
	}
}

int main()
{
     
	while(cin>>n>>m>>p1>>p2){
     
		memset(vis,0,MAXN*MAXN);
		for(int i=1;i<=n;i++)
			for(int j=1;j<=m;j++)
				cin>>map[i][j];
		ans=0;
		dfs(p1,p2);
		cout<<ans;
	}
	return 0;
}

队列

队列是一种特殊的线性表,是一种数据结构,它的特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。 它最主要的特点是先进先出

对队列进行的最常见的操作分别是:1、插入,2、删除。对队列的操作就好像一群人在排队,排在离柜台最近的人称之为“队头”,离柜台最远的人称之为“队尾”。每新来一个人需要排队(假设我们都是有素质的人,不插队),新来的人应该排在队尾,对吧?这就是插入操作。而柜台的办公人员会给队头的人先办公,然后依次进行,队头的人被服务完,他便会离开,这就是删除操作。

如何在代码中定义队列?

  • queue< int> q; 定义一个整型队列

  • q.push(a); 将a元素插入到q队列的队尾

  • q.pop(); 将q队列的队头从队列中删除

  • q.front() q队列的队头元素

  • q.back() q队列的队尾元素

  • q.size() q队列的大小

  • q.empty() 返回值是1代表队列是空,否则队列不为空

    (如果你问一个人push的反义词是什么,他回答是pop的话,他一定是一位合格的程序员。)

广度优先搜索(BFS)

简述

广度优先搜索的英文简写是BFS(Breadth First Search),属于图论中搜索算法中的一种,它所遵循的搜索策略是尽可能“广”地搜索图。

模版

#include
#include
#include
#include
using namespace std;
const int maxn=100;
bool vst[maxn][maxn]; // 访问标记
int dir[4][2]= {
     0,1,0,-1,1,0,-1,0}; // 方向向量

struct State // BFS 队列中的状态数据结构
{
     
    int x,y; // 坐标位置
    int Step_Counter; // 搜索步数统计器
};
State a[maxn];

bool CheckState(State s) // 约束条件检验
{
     
    if(!vst[s.x][s.y] && ...) // 满足条件
        return 1;
    else // 约束条件冲突
        return 0;
}

void bfs(State st)
{
     
    queue <State> q; // BFS 队列
    State now,next; // 定义2 个状态,当前和下一个
    st.Step_Counter=0; // 计数器清零
    q.push(st); // 入队
    vst[st.x][st.y]=1; // 访问标记
    while(!q.empty())
    {
     
        now=q.front(); // 取队首元素进行扩展
        if(now==G) // 出现目标态,此时为Step_Counter 的最小值,可以退出即可
        {
     
            ...... // 做相关处理
            return;
        }
        for(int i=0; i<4; i++)
        {
     
            next.x=now.x+dir[i][0]; // 按照规则生成下一个状态
            next.y=now.y+dir[i][1];
            next.Step_Counter=now.Step_Counter+1; // 计数器加1
            if(CheckState(next)) // 如果状态满足约束条件则入队
            {
     
                q.push(next);
                vst[next.x][next.y]=1; //访问标记
            }
        }
        q.pop(); // 队首元素出队
    }
    return;
}

int main()
{
     
    ......
    return 0;
}

例题1.hrbust 1012

  • 题意:个数轴上,有一个农民位于的位置处,有一头牛位于的位置处,农民有三种走路方式:①若农民位于 x ,农民可以移动一步到 x−1 或 x+1 ②若农民位于 x ,农民可以跳跃到 2∗x 处,问:农民需要最少多少步抓住那头牛?
    *要求:Time Limit: 2000 MS , Memory Limit: 65536 K
  • 思路:基本上是一道BFS的入门题,我们从农民的起点开始广搜,通过农民的三种移动方式(、、)来向队列中插入节点,广搜到牛的位置即可,具体细节看代码吧。
#include 
#include 
using namespace std;
int ans,step;
int st,end;
const int MAXN=1e5+1;
int vis[MAXN];
int queue[MAXN];

bool checke(int n)
{
     
	if (n>=0 && n<=MAXN && vis[n]==-1)
	return true;
	else 
	return false;
}

int bfs(int st,int end){
     
	int front=0,back=0,now;
	queue[back++]=st;
	vis[st]=0;
	while(front<=back){
     
		now = queue[front++];
		if(now==end) return vis[end];
		if(checke(now-1)){
     
			vis[now-1]=vis[now]+1;
			queue[back++]=now-1;
		}
		if(checke(now+1)){
     
			vis[now+1]=vis[now]+1;
			queue[back++]=now+1;
		}
		if(checke(now*2)){
     
			vis[now*2]=vis[now]+1;
			queue[back++]=now*2;
		}
		
	}
}


int main()
{
     
	while(cin>>st>>end){
     
		ans=0;
		memset(vis,-1,MAXN);
		cout<<bfs(st,end)<<endl;
	}
	return 0;
} 

例题2.走迷宫!

#include 
#include 
#include 
using namespace std;

int a[6][5]={
     
    0,0,1,0,0,
    0,1,0,0,0,
    0,1,0,1,1,
    0,1,0,0,0,
    0,0,0,1,0,
    0,0,0,0,0
			 };
int dir[4][2]={
     0,1,0,-1,1,0,-1,0}; //dir控制方向 

struct State{
     		//用state保存该点以及当前步数 
    int x,y=0;
    int stepCount=0;
};

int bfs(State st,State _end)	// bfs核心部分 
{
     
    queue <State> q;	//建立队列 
    State now,next;		
    q.push(st);			//将起点入队 
    a[st.x][st.y]=1;	//入队后重置该点为走过的点(此句为起点单独列出) 
    while(!q.empty())
    {
     
        now=q.front();	
        q.pop();
        if(now.x==_end.x && now.y==_end.y)	//到达终点,返回步数 
            return now.stepCount;
        for(int i=0;i<4;i++)
        {
     
            next.x=now.x+dir[i][0];
            next.y=now.y+dir[i][1];
            if(next.x<0 || next.y<0 || next.x>=6 || next.y>=5 || a[next.x][next.y]==1)
                continue;
            a[next.x][next.y]=1;
            next.stepCount=now.stepCount+1;
            q.push(next);
         /*   for(int j=0;j<6;j++)
            {
                for(int k=0;k<5;k++)
                    cout<
        }  
    }
}

int main()
{
     
    State beg,e;
    beg.x=0,beg.y=0,beg.stepCount=0;
    e.x=0,e.y=4;

    cout<<bfs(beg,e);
    return 0;
}

最短路 Dijkstra

求一个图中两个点之间最短距离的最快的一种方法,要求路径的花费不能有负的。

模板代码

#include 
#include 
using namespace std;

MAXN=100;
int dis[MAXN]; //与起点的距离 
int vis[MAXN]; //访问标记 
int a[MAX][MAXN] //a[m][n]为m到n之间的距离 

void Dij(int n)	//默认起点为1 
{
     
	memset(dis,MAXN,sizeof(dis));
	vis[1]=1;
	dis[1]=0;
	for(int i=1;i<=n;i++){
     
		int k=0;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&(k==0 || dis[j]<dis[k]))
				k=j
		v[k]=1;
		for(int j=1;j<=n;j++)
			if(!vis[j]&&dis[k]+a[k][j]<dis[j])
				dis[j]=dis[k]+a[k][j];
	}
}

(了解:Dijkstra优化)

#include 
#include 
#include 
#include 
#include 
using namespace std;

const int INF=0x3f3f3f3f;
const int MAXN=100001;

struct qnode{
     
	int v,c;					//v代表起点,c代表当前路径长 
	qnode(int _v=0,int _c=0):v(_v),c(_c){
     }
	bool operator < (const qnode &r) const	//重载来排序优先队列 
	{
     
		return c>r.c;
	}
};

struct Edge{
                	//邻接矩阵 
	int v,cost;			//v代表指向的点,cost代表路径 
	Edge(int _v=0,int _cost=0):v(_v),cost(_cost){
     }
};
vector<Edge> E[MAXN];		//E[i][j]代表从i指向j,E[i][j].cost为两者间路径, E[i][j].v代表指向的点j。 

bool vis[MAXN];			//是否访问标志 
int dist[MAXN];			//存放距离,起点到每个点的最短距离 

void Dijkstra(int n,int start) 			//n为结点个数,start为选取的起点 
{
     
	memset(vis,false,sizeof(vis));
	for(int i=1;i<=n;i++)
		dist[i]=INF;
	priority_queue<qnode> que;
	while(!que.empty()) que.pop();
	dist[start]=0;
	que.push(qnode(start,0));
	qnode tmp;
	while(!que.empty())
	{
     
		tmp=que.top();
		que.pop();
		int u=tmp.v;				//u为当前队首作为起点 
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=0;i<E[u].size();i++)		//E[u].size()等价于节点数 
		{
     
			int v=E[u][i].v;				//v为指向的下一个节点 
			int cost=E[u][i].cost;			//cost为当前起点u到目标点v的已知长度 
			if(!vis[v]&&dist[v]>cost+dist[u])		//比较起点直接到v点,和先到u点再到v点的路径 
			{
     
				dist[v]=dist[u]+cost;
				que.push(qnode(v,dist[v]));			///将目标点塞入队列,进行下一次松弛 
			}
		}
	}
}

void addedge(int u,int v,int w)  			//用于初始化邻接矩阵 
{
     
	E[u].push_back(Edge(v,w));
}

int main()
{
     
	/*  2 
	P1--->P3 
 1	↓     ↓4
	P2--->P4 
	   4
	*/ 
	
	int n=4;
	int v,w;
	freopen("in.txt","r",stdin); //输入输出分别在in.txt和out.txt中; 
	freopen("out.txt","w",stdout);
	for(int i=1;i<=n;i++)
	{
     
		addedge(i,0,-1);		//使得开始点为1,而不是0。 
		for(int j=1;j<=n;j++)
		{
     
			cin>>w;
			if(w==-1)			//无限用-1输入 
				addedge(i,j,INF); 
			else
				addedge(i,j,w);
		} 
	}
	
/*	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cout<
	
	Dijkstra(4,1);
	for(int i=1;i<=4;i++)
		cout<<dist[i]<<' '; 
	return 0;
} 

/*
	in.txt:	0 1 2 -1
			1 0 -1 4
			2 -1 0 4
			-1 4 4 0   
			
	out.txt标准答案: 0 1 2 5 
*/

KMP

KMP是一种在线性时间内能处理两个字符串的包含关系的算法,例如求一个字符串里有没有另一个字符串,一个字符串里有几个另一个字符串

模板代码

#include 
#include 
using namespace std;

int next[101];

void getNext(int* next,char* p){
     
	int i,n,k;
	
	n=strlen(p);
	next[1]=next[0]=0;
	k=0;
	for(i=2;i<=n;i++){
     
		for(;k!=0&&p[k]!=p[i-1];k=next[i]);
		if(p[k]==p[i-1])k++;
		next[i]=k;		
	}
}

void kmp(char* text,char* p,int* next)
{
     
	int len_t,len_p,s,q;
	len_t=strlen(text);
	len_p=strlen(p);
	q=s=0;
	// q表示上次迭代匹配了多少字符,s表示从总字符的第几位开始 
	while(s<len_t){
     
		for(q=next[q];q<len_p&&p[q]==text[s];q++,s++);
		if(q==0)s++;
		else if (q==len_p){
     
			cout<<"匹配成功,在原文的第"<<s-len_p+1<<"处开始。"<<endl; 
	//		q=0; 加上则为不可重叠 
		}
	}
}

int main()
{
     
	char p[]="cac";
	char text[]="cacaca";

	getNext(next,p);
	kmp(text,p,next);
	
	return 0;
}

Manacher

模板代码

#include 
#include 
using namespace std;

const int MAXN = 1e5+1;
char S[MAXN],T[MAXN];
int P[MAXN];

void manacher(char S[], int len)
{
     
	int k = 0;
	T[k++]='$',T[k++]='#';
	for(int i=0;i < len; i++)
	{
     
		T[k++]=S[i];
		T[k++]='#';
	}
	T[k]='0';
	
	int r=0,c=0;	//r代表当前对称的最远距离,c代表该对称的中心 
	for(int i = 0;i < k; i++)
	{
     
		int &p=P[i];
		p = r > i ? min(P[2*c-i],r-i) : 0; 		//P[2*c-i] --> 对称点x + i = 2*c 对称点坐标为P[2*c-i] 
		while(T[i+p+1]==T[i-p-1]) p++;
		if(i + p > r) {
     r = i + p; c = i;}		//如果i+p>r 有更大的扩展范围,则把i设为新中心,r为新半径。 
	}
	
/*	for(int i=0;i
	
	// 其中原字符串的开头下标 = (i - P[i])  / 2  只要返回 开头 到 开头+P[i]-1 之间的字符串 
 } 
 
int main()
{
     
	int n;
	cin>>n; 
	for(int i=0;i<n;i++)
	{
     
		cin>>S[i];
	}
	manacher(S,n);

	return 0;
} 

字典树

简述

​ 字典树,又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。
  字典树与字典很相似,当你要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找.字典树不仅可以用来储存字母,也可以储存数字等其它数据。

模板代码

#include 
#include 
using namespace std;

const int MAX=26,base='a';
char s1[12],ss[12];

struct Trie{
     
	int num;
	bool flag;
	Trie *son[MAX];
	Trie()
    {
     
        num=1;flag=false;
        memset(son,NULL,sizeof(son));
    }
}; 

Trie *NewTrie()
{
     
	Trie *temp = new Trie;
	return temp;
}

void Insert(Trie *root,char *s)
{
     
	Trie *temp = root;
	while(*s){
     
		if(temp->son[*s-base]==NULL)
			temp->son[*s-base]=NewTrie();
		else
			temp->son[*s-base]->num++;
		temp=temp->son[*s-base];
		s++;
	}
	temp->flag=true;
}

int Search(Trie *root,char *s)
{
     
	Trie *temp = root;
	while(*s){
     
		if(temp->son[*s-base]==NULL) return 0;
		temp = temp->son[*s-base];
		s++;
	}
	return temp->num;
}

int main()
{
     
	Trie *root = NewTrie();
	root->num=0;
	while(gets(s1)&&strcmp(s1,"")!=0)  
    {
     
        if(strcmp(s1," ")==0)
        	break;
        Insert(root,s1);
    }
    while(cin>>ss)
    {
     
        int  ans=Search(root,ss);
        cout<<ans<<endl;
    }
	return 0;
}

————————————————————————————————————————————

——by 创新实践部 钱舟毅

dp(动态规划)

动态规划是一种常用的分析办法,其分析的思维可以推广到许多的问题上去但是动态规划是具有使用条件的,主要是以下三点
最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

基本思路

1.分析最优解的性质,并刻画其结构特征。

2.递归的定义最优解。

3.以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值

4.根据计算最优值时得到的信息,构造问题的最优解

例题

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 5 1

对于一个斐波那契数列,我们可以得到一个常用的递推公式

a[i][j] = a[i-1][j-1]+a[i-1][j]

通过这个表达式我们就可以通过小的子问题去求出最后我们想要的结果


接下来给出dp算法的基本框架(不是正确的代码语法)

for(j=1; j<=m; j=j+1) // 第一个阶段
   xn[j] = 初始值;

 for(i=n-1; i>=1; i=i-1)// 其他n-1个阶段
   for(j=1; j>=f(i); j=j+1)//f(i)与i有关的表达式
     xi[j]=j=max(或min){
     g(xi-1[j1:j2]), ......, g(xi-1[jk:jk+1])};

t = g(x1[j1:j2]); // 由子问题的最优解求解整个问题的最优解的方案

print(x1[j1]);

for(i=2; i<=n-1; i=i+1{
       
     t = t-xi-1[ji];

     for(j=1; j>=f(i); j=j+1)
        if(t=xi[ji])
             break;
}

从上面的框架我们可以知道,dp算法的本质是将整个问题从最小的子问题从上而下不断进行计算,当我们需要第3行第2列的元素时,我们只需要从第2行第1列和第2行第2列的单元格进行取值,而这些元素已经在之前计算过了,所以我们可以直接算出结果

来看一个生活中常见的问题吧

  • 0-1 背包问题:给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为vi.
    问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

分析一波,面对每个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。

解决办法:声明一个 大小为 m[n][c]的二维数组,m[i][j]表示在面对第 i 件物品,且背包容量为 j 时所能获得的最大价值 ,那么我们可以很
容易分析得出 m[i][j] 的计算方法,

(1)j < w[i] 的情况,这时候背包容量不足以放下第 i 件物品,只能选择不拿

m[ i ][ j ] = m[ i-1 ][ j ]

(2)j>=w[i] 的情况,这时背包容量可以放下第 i 件物品,我们就要考虑拿这件物品是否能获取更大的价值。

如果拿取,m[ i ][ j ]=m[ i-1 ][ j-w[ i ] ] + v[ i ]。 这里的m[ i-1 ][ j-w[ i ] ]指的就是考虑了i-1件物品,背包容量为j-w[i]时的最大价值,也是相当于为第i件物品腾出了w[i]的空间。

如果不拿,m[ i ][ j ] = m[ i-1 ][ j ] , 同(1)

究竟是拿还是不拿,自然是比较这两种情况那种价值最大。

由此可以得到状态转移方程:

if(j>=w[i])
    m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
else
    m[i][j]=m[i-1][j];

在使用动态规划算法求解0-1背包问题时,使用二维数组m[i][j]存储背包剩余容量为j,可选物品为i、i+1、……、n时0-1背包问题的最优值。绘制
价值数组v = {8, 10, 6, 3, 7, 2},

重量数组w = {4, 6, 2, 2, 5, 1},

背包容量C = 12时对应的m[i][j]数组。

0	1	2	3	4	5	6	7	8	9	10	11	12
1	0	0	0	8	8	8	8	8	8	8	8	8
2	0	0	0	8	8	10	10	10	10	18	18	18
3	0	6	6	8	8	14	14	16	16	18	18	24
4	0	6	6	9	9	14	14	17	17	19	19	24
5	0	6	6	9	9	14	14	17	17	19	21	24
6	2	6	8	9	11	14	16	17	19	19	21	24
  • (第一行和第一列为序号,其数值为0)
    m[2][6]在面对第二件物品,背包容量为6时我们可以选择不拿,那么获得价值仅为第一件物品的价值8,如果拿,就要把第一件物品拿出来,放第二件物品,价值10,那我们当然是选择拿。m[2][6]=m[1][0]+10=0+10=10;依次类推,得到m[6][12]就是考虑所有物品,背包容量为C时的最大价值。

  • #include 
    #include 
    using namespace std;
     
     
    const int N=15;
     
     
    int main()
    {
           
        int v[N]={
           0,8,10,6,3,7,2};
        int w[N]={
           0,4,6,2,2,5,1};
     
     
        int m[N][N];
        int n=6,c=12;
        memset(m,0,sizeof(m));
        for(int i=1;i<=n;i++)
        {
           
            for(int j=1;j<=c;j++)//双重循环控制正向递推
            {
           
                if(j>=w[i])
                    m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);//递推关系式
                else
                    m[i][j]=m[i-1][j];
            }
        }
     
     
        for(int i=1;i<=n;i++)
        {
           
            for(int j=1;j<=c;j++)
            {
           
                cout<<m[i][j]<<' ';
            }
            cout<<endl;
        }//打印输出二维表格,同时也打印出需要的单元数据
     
     
        return 0;
    }
    

你可能感兴趣的:(软件,算法)