赞
踩
任何问题都有涉及的范围(状态空间),求解问题就是在这个状态空间里遍历与映射。遍历顺序、遍历中的决策方法、状态空间中各种状态的之间的映射方式。枚举、模拟是按照问题直接表述形式对状态空间最朴素的遍历;搜索是带有一定选择性、决策性的遍历;贪心是在每步决策时采用局部最优策略的遍历;动态规划是基于全局考量的分阶段、按维度、无重复遍历;二分、倍增、排序可以对状态空间进行划分、等价、代表、拼接等手段,直接降低遍历时需要面对的时空规模。
(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧(๑•̀ㅂ•́)و✧
朴素遍历->空间需要优化:用时间换空间/有些不用存;
->时间要优化-> 找规律->离线处理、数据结构(数据结构的设计思路来源于各种基本算法思想(优化的过程),分为线性数据结构和树形数据结构,思考数据结构是如何优化的)
状态空间(实际问题的各种可能情况构成的集合)就是,长的和解形式一样但是内容不一样的东西,先确定解的形式和内容,依此考虑状态空间。例如:任意时刻的局面就是状态,任意时刻哪些点没被经过哪些点已经被经过,状态里要有全局的轮廓(已经经过的点),也要有当前的位置(现在在哪个点)
表示系统在现在、过去、未来时刻的状况,由一组变量描述
a. 先确定所有的状态(状态空间,如果找不见考虑模拟过程)(有时候要分类讨论、以(扫描的)当前状态为视角),想到朴素的枚举遍历(就是在这个解空间里找到我需要的解),然后从答案(解)出发,找我需要的信息去维护;如果题目描述的解不好处理就换一个角度转化一下题目或者建立模型去思考(怎么转化这个也要积累),如:如果数据差特别大,选择数据小的这一元素进行枚举
b. 二维的会枚举一维,另一维优化枚举
c. 优化就是:跳过或剪去我已经知道的不是解的情况/用数据结构集中有用的信息和我需要的信息(了解数据结构可以维护哪类信息、怎么维护、怎么跳过冗余状态的)/找到有冗余、拖慢计算速度的状态处理掉/重复的记下来下次用
算法和数据结构通过划分、归纳、提取、抽象提高遍历的效率,如何划分归纳?考虑题目的性质,子问题,平凡情况,找到内在规律,转化为与原问题等价的问题
d. 做题时逐步优化的过程就是经验要积累下来
e. 有些是自己想不到的优化方式,一般采用已有的算法求解
f. 各种模型之间的简化、拓展、联系
现在问题就是想不到除了朴素地遍历,还能用什么优化方法遍历
所以跑来积累一下ヽ(✿゚▽゚)ノ
……有时候连状态都枚举不出来,状态是啥都不知道……
g. 如果找不出状态则往模拟上面考虑
h. 正向不易考虑,从逆向考虑等价的情况,如删去变为选择、uva11925(从1234……变为给定的是1对多的映射关系,但是从给定的变成1234……是1对1,比较容易,两种操作是为了将大的换到后面)
从枚举的角度出发,思考要枚举什么,什么顺序枚举,以枚举的顺序这种动态的开点思想
0. 子问题和平凡的情况是两回事,平凡的情况是原问题的一种情况(状态),子问题不是原问题,而是原问题的化简,缩小规模后的问题
int go(int p, int d, int step) {
while(step--) {
do {
p = (p + d + n - 1) % n + 1;//p位置按方向d走一格,然后绕一圈到下一个位置,再加一
} while(a[p] == 0);
}
return p;
}
状态:“多少种合法位置方案”->具体合法方案就是状态
充分考虑题目里的已知性质,对于答案有什么影响,是否可以排除掉某些状态
状态是用多个维度去描述一种情况,状态的设计一般从解的结构形式和内容(维度的内涵)考虑,更像描述一种存放题目中各个具体元素的容器(poj 2279)
不想一个一个的枚举,要优化速度可以:二分、倍增、预处理、排序、
一般求逆序对时使用。什么情况是求逆序对呢?
1、冒泡排序的次数即为逆序对个数
2、一个数组置换、交叉(即不同的排列)后,可能考虑逆序对
针对区间的问题:1 连续的一段 2 对该段有条件限制 即:需要在给的一组数据中找到不大于某一个上限的“最优连续子序列”
思路:l=0,r=1;l每右移一个,就去找r的合法位置 poj 3061
for(int l=1,r=0,now=0;l<=n;){
while(now<k&&r<n) now+=a[++r];//r<n,若是r<=n,则r会越界
if(now<k) break;
ans=min(ans,r-l+1);
now-=a[l++];
}
if(ans==inf) ans=0;
状态空间特别大,答案范围也特别大的时候就要考虑二分
注意答案范围:l=0,r=inf
(double 用eps,精度可能会炸,int 用 l<r )
bool judge(double x){
rep(i,1,n+1) tmp[i]=a[i]-x;
rep(i,1,n+1) sum[i]=sum[i-1]+tmp[i];
double ans=-1e10;
double mival=1e10;
rep(i,f,n+1){
mival=min(mival,sum[i-f]);//不用两层循环,左端一直选最小值,右端一直选最大值即可
ans=max(ans,sum[i]-mival);
}
if(ans>=0) return true;
else return false;
}
bool judge(double r){ double xl=-inf,xr=inf; rep(i,0,n){ double d=fabs(p[i].y-r); if(d>r) return false; double xxl,xxr; xxl=p[i].x-sqrt(r-d)*sqrt(r+d); xxr=p[i].x+sqrt(r-d)*sqrt(r+d);//先乘再sqrt会炸精度,sqrt数越大精度越差,所以先开方再乘 if(xr<xxl||xxr<xl) return false; xl=max(xl,xxl); xr=min(xr,xxr); } return true; } double l=0.0,r=10000000000000000; // 按循环次数二分 rep(i,0,100){ double mid=(l+r)/2.0; if(judge(mid)){ r=mid; } else l=mid; } printf("%.6f\n",r);
bool dfs(int u,int vv,int color1) { if(!vis[u]) { vis[u] = 1; if(u == vv) return true; for(int i = 0 ; i < G[u].size(); i++) { int v = G[u][i]; for(int j = 1 ; j <= vis1[u][v]; j++)//对所有重边都讨论 { if(color[u][v][j] == color1) { if( dfs(v, vv, color1)) return true; } } } } return false ; } for(int i= 1; i <= m; i++) { scanf("%d%d%d", &a, &b, &c); G[a].push_back(b); G[b].push_back(a); vis1[a][b]++; vis1[b][a]++; color[a][b][vis1[a][b]] = c; color[b][a][vis1[b][a]] = c; } scanf("%d", &k); int v1, v2; while(k--) { sum = 0; scanf("%d%d", &v1, &v2); for(int i = 1; i <= m; i++) { memset(vis, 0, sizeof(vis)); if(dfs(v1, v2, i)) { sum++; } } printf("%d\n", sum); }
2.3 树的大小
a.给你一棵N个结点的树,问最多剪掉多少条边之后,剩下的连通分量的size都为偶数,如果一条也没法剪掉输出-1。思路:按奇偶讨论,什么情况-1?奇数;否则就遇到偶数的子树就剪掉。dfs求树的大小。
int dfs(int x) {
int son=0; //计算子节点个数
vis[x]=1;
for(int i=0; i<v[x].size(); ++i) {
if(!vis[v[x][i]]) {
son+=dfs(v[x][i]); //对每个没搜过的节点搜,记录子树顶点。 }
}
if((son+1)%2==0)
ans++; //偶数点的就++,因为整棵树还要加上根节点1个,所以son要+1。
return son+1;
}
}
有合并、优化两个功能,用根代表整个集合,遍历森林就是遍历每个根,这样不用遍历集合里有什么所以降低了复杂度。因此时刻都想着根,用根表示集合和集合的各种信息。
int f[200][200]; int getf(int v,int w) //就是多了w,其他的都不变 { if(f[v][w]==v) return v; else { f[v][w]=getf(f[v][w],w); return f[v][w]; } } void merge(int v,int u,int w) { int t1=getf(v,w); int t2=getf(u,w); if(t1!=t2) f[t2][w]=t1; } //初始化的时候就相当于 for(int i=1; i<=n; i++) for(int j=1; j<=m; j++) f[i][j]=i; int a,b,c,q,x,y; for(int i=0; i<m; i++) { scanf("%d%d%d",&a,&b,&c); merge(a,b,c); } scanf("%d",&q); for(int i=1; i<=q; i++) { int ans=0; scanf("%d%d",&x,&y); for(int j=1; j<=m; j++) { int a=getf(x,j); int b=getf(y,j); if(a==b)ans++; } printf("%d\n",ans); }
3.2. 维护二维地图上的联通块(dfs也可)
一般二维地图还是采用搜索比较好。
5. 带权并查集(路径压缩时统计点到根的路径上的信息)
6. 维护传递关系(floyd)
判断图的形状:考虑点的度数、叶子结点开始、联通块个数、点数与边数的关系等等性质
a. cf 103B (判断一个图是不是由>=3棵树构成,这些树的根在同一个环上。思路:仅有一个连通块,且边数==n才只有一个简单环)
b. 求每个点到唯一环的距离 cf 131D
传递关系 floyd更新传递关系里的花费最值 poj1125
二分图匹配:关键是如何识别如何建图
a. 完美匹配(最大匹配,匹配组数最多)匈牙利算法
b. 最优匹配(匹配以后权值最右,不一定最大)km算法
c. 最小点覆盖
d. 最小边覆盖
e. 最大独立集
dijsktra 及堆优化
拓扑序 将一种关系看成是一条有向/无向边
欧拉路 dfs实现,找奇数度的点 欧拉路的条件
构造欧拉回路:对每一个连通图来说,如果有2k个度为奇数的节点,那+k-1条边可以使它成为欧拉回路;如果有2k+1个度为奇数的节点,那+k-1条边可以使它成为欧拉路径。
uva12118
分析:当每条边仅经过一次时,路径最短。给出的边可能构成若干棵树。
在一棵树中,奇点个数总为偶数,若一棵树的奇点个数为0,则这棵树可以构成欧拉回路,若不为0,则必有走不到的边(每条边仅经过一次,下同)。
在一棵树中,设奇点个数为n,则走不到的边数为(n-2)/2 (n-2为除去起点和终点的奇点个数),这意味着,还需要走额外的(n-2)/2条边才能将这(n-2)/2条指定的但走不到的边走完。并且,这(n-2)/2条走不到的边是不共点的,这意味着,一棵树还是多棵树是无关紧要的。但是,如果有的树中奇点个数恰为0,没有走不到的边,此时这棵树成了孤立的了,要注意这种情况。(所以就给他加两个奇数点,让这个联通块也能和其他块连上) 代码里max(0,(ans-2)/2)是指如果有0个奇点则已经是欧拉回路不需要加边,特判n=0的情况
拓扑排序(判环、处理信息冲突问题)
拓扑排序处理信息不完整和冲突
1.如果出现两个同时入度为0的,并且根是自己就是信息不完整。
2.至于信息冲突的情况就是出现了环,这时候会发现查询入度为0的循环执行不下去了,也就是sum>0.
邻接矩阵幂:A^m 所代表的意义就是从点与点之间走m步能够到达的方案总数
//当前弧优化、多路增广 struct dinic { struct node { int to, nxt, w; void get(int a, int b, int c) { to = a, w = b, nxt = c; } } edge[maxm << 1]; int s, t, dep[maxn]; int head[maxn], tot; void init() { tot = 0; memset(head, -1, sizeof(head)); } void addedge(int u, int v, int w) { edge[tot].get(v, w, head[u]), head[u] = tot++; edge[tot].get(u, 0, head[v]), head[v] = tot++; } int q[maxm]; bool bfs() { int fr = 0, tail = 0; memset(dep, -1, sizeof(dep)); dep[s] = 0; q[tail++] = s; while(fr < tail) { int u = q[fr++]; for(int i = head[u]; ~i; i = edge[i].nxt) { int v = edge[i].to; if(dep[v] == -1 && edge[i].w) { dep[v] = dep[u] + 1; if(v == t) return 1; q[tail++] = v; } } } return 0; } int dfs(int u, int fl) { int flow = 0; if(fl == 0 || u == t) return fl; for(int i = head[u]; ~i; i = edge[i].nxt) { int v = edge[i].to; if(dep[u] + 1 == dep[v] && edge[i].w) { int f = dfs(v, min(fl, edge[i].w)); if(f > 0) { edge[i].w -= f; edge[i^1].w += f; flow += f; fl -= f; if(!fl) break; } } } if(!fl) dep[u] = -1; return flow; } int dinic() { int flow = 0; while(bfs()) { flow += dfs(s, inf); } return flow; } } di; struct Dinic { static const int maxn = 1e6+5; static const int maxm = 4e6+5; struct Edge { int u, v, next, flow, cap; } edge[maxm]; int head[maxn], level[maxn], cur[maxn], eg; void addedge(int u, int v, int cap) { edge[eg]={u,v,head[u],0,cap},head[u]=eg++; edge[eg]={v,u,head[v],0, 0},head[v]=eg++; } void init() { eg = 0; memset(head, -1, sizeof head); } bool makeLevel(int s, int t, int n) { for(int i = 0; i < n; i++) level[i] = 0, cur[i] = head[i]; queue<int> q; q.push(s); level[s] = 1; while(!q.empty()) { int u = q.front(); q.pop(); for(int i = head[u]; ~i; i = edge[i].next) { Edge &e = edge[i]; if(e.flow < e.cap && level[e.v] == 0) { level[e.v] = level[u] + 1; if(e.v == t) return 1; q.push(e.v); } } } return 0; } int findpath(int s, int t, int limit = INT_MAX) { if(s == t || limit == 0) return limit; for(int i = cur[s]; ~i; i = edge[i].next) { cur[edge[i].u] = i; Edge &e = edge[i], &rev = edge[i^1]; if(e.flow < e.cap && level[e.v] == level[s] + 1) { int flow = findpath(e.v, t, min(limit, e.cap - e.flow)); if(flow > 0) { e.flow += flow; rev.flow -= flow; return flow; } } } return 0; } int max_flow(int s, int t, int n) { int ans = 0; while(makeLevel(s, t, n)) { int flow; while((flow = findpath(s, t)) > 0) ans += flow; } return ans; } } di; int ans = di.max_flow(s, t, 点个数+10);
之前是什么状态,回来的时候要一模一样的。
dp就是在减小问题规模,dfs(x)就是问题规模不是n了而是x的答案
图上的dp都用记忆化搜索!!!
记a1,a2,……为规模为1,2,……的问题的解,则动态规划是用a1,a2……ai-1去推导ai
贪心是用ai-1推导ai
概述:
解决多阶段决策(当前这个选不选)问题,即每个阶段都要做一个决策,全部的决策是一个决策序列,要你求一个最好的决策序列使得这个问题有最优解。
将待求解的问题分为若干个相互联系的子问题,只在第一次遇到的时候求解,然后将这个子问题的答案保存下来(记忆化),下次又遇到的时候直接拿过来用即可。
dp在选择的时候是从以前求出的若干个与本步骤相关的子问题中选最优的那个,加上这一步的值来构成这一步那个子问题的最优解。
具体想法:(记忆化搜索)
贪心主要就是在证明问题整体最优可以由局部最优推导出来。策略考虑当前这一步的策略的作用范围扩展到后续状态产生的影响
贪心法是每步选择局部最优解,然后一步一步向目标迈进。这个“目标”两字很关键,说明贪心法是目标导向的,每一步的方向一定是目标
void getol() { mm(isprim,0); phi[1]=0; int r=0; for(int i=2;i<maxn;i++) { if(!isprim[i]) { prime[r++]=i; phi[i]=i-1; } for(int j=0;j<r&&i*prime[j]<maxn;j++) { isprim[i*prime[j]]=1; if(i%prime[j]==0) { phi[i*prime[j]]=phi[i]*prime[j]; break; } else { phi[i*prime[j]]=phi[i]*(prime[j]-1); } } } }
大一点的数求欧拉函数值
ll phi(ll n){
ll ans=n;
for(int i=2;i<=sqrt(n);i++){
if(n%i==0){
ans=ans/i*(i-1);
while(n%i==0)
n/=i;
}
}
if(n>1)//n是质数
ans=ans/n*(n-1);
return ans;
}
int id[1010]; struct node { int fa, lch, rch; char ans[1010]; int val, len; }tree[maxn];//从1开始有效 int rt, tot; void init(){ rt = 0; tot = 0; tree[0].ans[0] = '\0'; tree[0].fa = 0; tree[0].lch = tree[0].rch =0; tree[0].len = -1; } char judge(int x) { int fa = tree[x].fa; if(tree[fa].rch == x) return 'W'; else return 'E'; } void insrt(int &now, int fa, int val) { //还没有过这个点 printf("now1: %d\n", now); if(now == 0) { now = ++tot; printf("now2: %d\n", now); tree[now].fa = fa; tree[now].lch = tree[now].rch = 0;//作为下一个点的now,类似空指针 tree[now].val = val; rep(i, 0, tree[fa].len) { tree[now].ans[i] = tree[fa].ans[i]; } tree[now].len = tree[fa].len + 1; //fa不是根 int tmp = tree[now].len - 1; if(fa) tree[now].ans[tmp] = judge(tot); tree[now].ans[tmp + 1] = '\0'; return; } if(val < tree[now].val) insrt(tree[now].lch, now, val);//这次就传当前点的lch,所以修改的就是lch,不是rt了 else insrt(tree[now].rch, now, val); } int main(){ //freopen("../result.txt","w",stdout); int t; sd(t); while(t--) { int n; sd(n); init(); rep(i, 0, n) { int val; sd(val); printf("rt: %d\n", rt); insrt(rt, 0, val);//传这个rt以后,因为是传引用,所以rt的值被修改了,每次都变成++tot } rep(i, 1, tot + 1) id[tree[i].val] = i; rep(i, 0, tot + 1) { printf("%d: val: %d fa: %d lch: %d rch: %d\n", i, tree[i].val, tree[i].fa, tree[i].lch, tree[i].rch); } int q; sd(q); while(q--) { int x; sd(x); printf("%s\n", tree[id[x]].ans); } } return 0; }
!!!动态开点的所有fa,lch,rch的表示都用tree[maxn]里的下标,id用来映射结点的val值(一般用于表示点权)对应的tree中的下标
3. 字典树
用于统计大量字符串、查询某个字符串、关于字符串的子串、前缀等
4. 树状数组
x+=lowbit(x) 从x一直上到他的祖先(x<=n)(数字越来越大)
x-=lowbit(x) 从x一直下到他的叶子结点(x>0)(数字越来越小)
a. 求 i 位置前(后可以per求)有多少个比他小(大可以用i-1-x计算出来)的数,原数组是cnt[i],表示i的个数
最大限度地减少无谓的字符串比较
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。