- 简介
- 搜索
- 迷宫(BFS+队列)
- 最短路
- Dijkstra+邻接矩阵
- Dijkstra+链式前向星+优先队列
- Bellman-Ford + 链式前向星
- Bellman-Ford(标准)
- Floyd
- SPFA+邻接矩阵
- SPFA+DFS
- 最短路例题
- 生成树
- Kruskal
- Prim
- 次小生成树例题
- 并查集
- 一般并查集
- 带权并查集
- 并查集例题
- 线段树
- 漂浮法
- 线段树例题
- 动态规划
- 最长公共子序列
- 最长非降子序列(朴素)
- 最长非降子序列(二分)
- 最大字段和
- 递推例题
- 01背包
- 初始化的细节问题
- 完全背包问题
- 多重背包问题
- 数位DP例题
- 状压DP
- 树形DP
- 数学
- GCD与LCM
- 简易扩展欧几里得算法
- 杂项模板
- 万金油小算法
- STL用法
- 其他
简介
自用模板。
尽量选用题目来对模板进行解释。
搜索
迷宫(BFS+队列)
用结构体作为队列元素,三维数组存储地图。找到起点,入队,遍历前后左右上下移动,分别入队。用vis
数组进行判重。
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
struct node{
int x, y, z;
int step;
};
int sx, sy, sz; // 储存起始点坐标
int to[6][3]{1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 1, 0, 0, -1}; // 移动方向
int L, R, C; // 长宽高
char map[30][30][30];
int cnt, vis[30][30][30];
int check(int x, int y, int z){ // check it is illegal or not
if(x<0 || x>=L || y<0 || y>=R || z<0 || z>=C) return 1;
if(vis[x][y][z] || map[x][y][z] == '#') return 1;
return 0;
}
void bfs(){
queue<node> q;
node a, b;
a.x = sx, a.y = sy, a.z = sz;
a.step = 0;
vis[a.x][a.y][a.z] = 1;
q.push(a);
while(!q.empty()){
a = q.front(); q.pop();
if(map[a.x][a.y][a.z] == 'E'){ // 遇到终点结束
cnt = a.step;
return;
}
for(int i=0; i<6; i++){ // 前后左右上下移动
b = a;
b.x += to[i][0];
b.y += to[i][1];
b.z += to[i][2];
if(check(b.x, b.y, b.z)) continue;
b.step = a.step + 1;
vis[b.x][b.y][b.z] = 1;
q.push(b);
}
}
cnt = -1;
}
int main(){
while(scanf("%d%d%d", &L, &R, &C), L){
for(int i=0; i<L; i++)
for(int j=0; j<R; j++)
scanf("%s", map[i][j]); // input
for(int i=0; i<L; i++)
for(int j=0; j<R; j++)
for(int k=0; k<C; k++)
if(map[i][j][k] == 'S'){
sx = i; sy = j; sz = k; // find start
}
memset(vis, 0, sizeof(vis)); // initial
bfs();
if(cnt == -1) { printf("Trapped!\n"); } // output
else printf("Escaped in %d minute(s).\n", cnt);
}
return 0;
}
最短路
Dijkstra+邻接矩阵
邻接矩阵设置为全局变量,无向边存储两次。结果是起点到所有点的最短距离数组,不能判负环。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1000;
int map[maxn][maxn];
int pre[maxn], dis[maxn];
bool vis[maxn];
int n,m;
void Dijkstra(int s){
memset(dis,0x3f,sizeof(dis));
memset(pre,-1,sizeof(pre));
memset(vis,false,sizeof(vis));
for(int i=1;i<=n;++i){
dis[i]=map[s][i];
pre[i]=s;
}
dis[s]=0;
vis[s]=true;
for(int i=2;i<=n;++i){
int mindist=INF;
int u=s;
for(int j=1;j<=n;++j)
if((!vis[j])&&dis[j]<mindist){
u=j; mindist=dis[j];
}
vis[u] = true;
for(int j=1;j<=n;++j)
if((!vis[j]) && map[u][j] < INF){
if(map[u][j] + dis[u] < dis[j]){
dis[j] = map[u][j] + dis[u];
pre[j] = u;
}
}
}
}
Dijkstra+链式前向星+优先队列
该模板需要初始化。
通过addnode
方法加边,无向边读取两遍。优先队列优化时间效率,链式前向星遍历某节点下所有边,同时优化空间效率。结果是一个从起点到所有点的最短距离数组。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <queue>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=1000;
struct node{
int d,u;
friend bool operator<(node a,node b){
return a.d>b.d;
}
node(int dist,int point):d(dist),u(point){}
};
struct Edge{
int to,next;
int dist;
}edge[maxm];
int head[maxn],tot;
int pre[maxn],dis[maxn];
void init(){
memset(head,-1,sizeof(head));
tot=0;
}
void addedge(int u,int v,int d){
edge[tot].to=v;
edge[tot].dist=d;
edge[tot].next=head[u];
head[u]=tot++;
}
void Dijkstra(int s){
priority_queue<node> q;
memset(dis,0x3f,sizeof(dis));
memset(pre,-1,sizeof(pre));
dis[s]=0;
while(!q.empty()) q.pop();
node a(0,s); q.push(a); //起点入队列
while(!q.empty()){
node x=q.top(); q.pop();
if(dis[x.u]<x.d) continue;
for(int i=head[x.u];i!=-1;i=edge[i].next){
int v=edge[i].to;
if(dis[v]>dis[x.u]+edge[i].dist){
dis[v]=dis[x.u]+edge[i].dist;
pre[v]=x.u;
q.push(node(dis[v],v));
}
}
}
}
Bellman-Ford + 链式前向星
我个人感觉这个算法的优化在于重边,以及松弛函数与主算法的分离。某些只针对松弛的题目变化可以很方便地使用。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
using namespace std;
struct Edge{
int u, v;
double r, c;
}edge[maxn*2];
double mostMoney[maxn];
int n, m, s;
double v;
int tot;
void addedge(int u, int v, double r, double c){
edge[tot].u = u;
edge[tot].v = v;
edge[tot].r = r;
edge[tot++].c = c;
}
bool relax(int n){
double temp = (mostMoney[edge[n].u] - edge[n].c)*edge[n].r;
if (temp > mostMoney[edge[n].v]){
mostMoney[edge[n].v] = temp;
return true;
}
return false;
}
bool bellman_ford(){
bool flag;
for (int i=0; i<n; i++) mostMoney[i] = 0.0;
mostMoney[s] = v;
for (int i=0; i<n-1; ++i){
flag = false;
for (int j=0; j<tot; ++j)
if (relax(j)) flag = true;
if (mostMoney[s] > v) return true;
if (!flag) return false;
}
for (int i=0; i<tot; ++i)
if (relax(i)) return true;
return false;
}
Bellman-Ford(标准)
用简单的结构体来存储数据、遍历。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=1000;
struct Edge{
int u,v; //起点、终点
int dist; //长度
}edge[maxn];
int dis[maxn]; //最短距离数组
int n, m; //结点数、边数
bool Bellman_ford(int s){
memset(dis, INF, sizeof(dis));
dis[s]=0;
for(int k=1; k<n; ++k){ //迭代n-1次
for(int i=0; i<m; ++i){ //检查每条边
int x = edge[i].u, y = edge[i].v;
if(dis[x] < INF)
dis[y] = min(dis[y], dis[x] + edge[i].dist);
}
}
bool flag=1;
for(int i=0; i<m; ++i){ //判断是否有负环
int x = edge[i].u, y = edge[i].v;
if(d[y] > d[x] + edge[i].dist){
flag = 0; break;
}
}
return flag;
}
Floyd
此算法三重循环是有顺序的。
最短路松弛算法为:
map[i][j] = min(map[i][k]+map[k][j], map[i][j])
最外层循环为中继点,第二次为起点,第三层为终点。
void floyd(){
for(int k=1; k<=n; ++k)
for(int i=1; i<=n; ++i)
for(int j=1; j<=n; ++j)
if (map[i][j] > map[i][k] + map[k][j]) //松弛
map[i][j] = map[i][k] + map[k][j];
}
SPFA+邻接矩阵
void spfa(int s){
for(int i=0; i<=n; i++) dis[i]=0x3f3f3f3f; //初始化每点到s的距离
dis[s]=0; vis[s]=1; q[1]=s; //队列初始化,s为起点
int i, v, head=0, tail=1;
while (head++<tail){ //队列非空
v=q[head]; //取队首元素
vis[v]=0; //释放队首结点,因为这节点可能下次用来松弛其它节点,重新入队
for(i=0; i<=n; i++) //对所有顶点
if (a[v][i]>0 && dis[i]>dis[v]+a[v][i]){
dis[i] = dis[v]+a[v][i]; //修改最短路
if (vis[i]==0){ //如果扩展结点i不在队列中,入队
q[++tail]=i;
vis[i]=1;
}
}
}
}
SPFA+DFS
用a[i][j]
存储从i
到j
所需要的距离。
用b[i][j]=w
存储从i
到w
是i
结点下的第j
条边。
void spfa(int s){
for(int i=1; i<=b[s][0]; i++) //b[s,0]是从顶点s发出的边的条数
if (dis[b[s][i]]>dis[s]+a[s][b[s][i]]){ //b[s,i]是从s发出的第i条边的另一个顶点
dis[b[s][i]=dis[s]+a[s][b[s][i]];
spfa(b[s][i]);
}
}
最短路例题
CSU - 1808
题意
最短路。
给出n个地铁站,m条边,给出每条边的首尾地铁站a、b
,和这条边所属的地铁线c
,以及这条边的边权d
。
地铁线之间需要换乘,换乘时间为abs(ci-cj)
。
因为多了一个换乘时间,所以需要拆点。
用链式前向星存边,用map<int,int>
拆点,用vector
存当前点所在的地铁。
思路
首先读边。用map[u][x]来表示u地铁站在x号线上,存储一个标记值cnt,代表这个点是第几个点(重新编号)。然后取出这个点。v点同样。最后addedge两遍。
遍历所有点。对每个点下的vector排个序,这样就可以避免取绝对值。对每个点遍历vector,对于vector中的每条地铁线,将这个点取出添边。
制图之后就按照模板跑Dijkstra,注意最后获取结果的时候要跑一遍n点的vector找最小值。
代码
/******************************
*File name: csu1808.cpp
*Author: wzhzzmzzy
*Created Time: 一 4/17 20:54:29 2017
*TODO: CSU 1808 最短路
******************************/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
#include <map>
using namespace std;
const int maxn = 300005;
const int inf = 0x3f3f3f3f;
int n, m;
struct Edge{
int to, next;
int w;
}edge[maxn<<1];
int head[maxn], tot;
void init(){
memset(head, -1, sizeof(head));
tot = 0;
}
void addedge(int u, int v, int w){
edge[tot].to = v;
edge[tot].w = w;
edge[tot].next = head[u];
head[u] = tot++;
}
vector<int> num[maxn];
//某站所属的线路
map<int,int> mmp[maxn];
//某站是记录的第几个地铁站(拆点)
int dis[maxn], cnt;
struct node{
int now, c;
node(int _n=0, int _c=0):now(_n), c(_c){}
friend bool operator <(node a, node r){
return a.c > r.c;
}
};
void Dijkstra(){
priority_queue<node> q;
while (!q.empty()) q.pop();
for (int i = 1; i < cnt; ++i) dis[i] = inf;
for (int i = 0; i < num[1].size(); ++i){
int st = mmp[1][num[1][i]];
dis[st] = 0;
q.push(node(st, 0));
}
node temp;
while (!q.empty()){
temp = q.top(); q.pop();
int u = temp.now;
int cost = temp.c;
if (cost > dis[u]) continue;
for (int i = head[u]; ~i; i=edge[i].next){
int v = edge[i].to;
int w = edge[i].w;
if (dis[v] > cost+w){
dis[v] = cost + w;
q.push(node(v, dis[v]));
}
}
}
}
int main(){
int u, v, x, w;
while (~scanf("%d%d", &n, &m)){
init();
cnt = 1;
for (int i = 1; i <= n; ++i){
num[i].clear();
mmp[i].clear();
}
for (int i = 0; i < m; ++i){
scanf("%d%d%d%d", &u, &v, &x, &w);
if (!mmp[u][x]){
mmp[u][x] = cnt++;
num[u].push_back(x);
}
u = mmp[u][x];
if (!mmp[v][x]){
mmp[v][x] = cnt++;
num[v].push_back(x);
}
v = mmp[v][x];
addedge(u, v, w);
addedge(v, u, w);
}
for(int i = 1; i <= n; ++i){
sort(num[i].begin(), num[i].end());
for(int j = 0; j < num[i].size()-1; ++j){
u = mmp[i][num[i][j]];
v = mmp[i][num[i][j+1]];
w = num[i][j+1] - num[i][j]; //同一站点不同线路的拆点之间的差值
addedge(u, v, w);
addedge(v, u, w);
}
}
Dijkstra();
int ans = inf;
for (int i = 0; i < num[n].size(); ++i){
u = mmp[n][num[n][i]];
ans = min(ans, dis[u]);
}
printf("%d\n", ans);
}
return 0;
}
生成树
Kruskal
用并查集来将所有的节
点组织成一棵树。不需要正反读无向边,默认无向。
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn;
int fa[maxn];
int n, m;
struct Edge{
int u, v, w;
}edge[maxn];
bool cmp(Edge a,Edge b){
return a.w < b.w;
}
int find(int x){
return fa[x] == x? x: fa[x] = find(fa[x]);
}
int Kruskal(){
int ans = 0;
for(int i=0; i<=n; ++i)
fa[i]=i;
sort(edge, edge+m, cmp);
for(int i = 0; i < m; ++i){
int x = find(edge[i].u);
int y = find(edge[i].v);
if(x != y){
ans += edge[i].w;
fa[x] = y;
}
}
return ans;
}
Prim
void prim(){
int mi, v = 0;
for(int i = 0; i < n; i++){
dis[i] = map[0][i];
vis[i] = false;
}
for(int i = 1; i <= n; i++){
mi = INF;
for(int j = 0; j < n; j++)
if(!vis[j] && mi > dis[j]){
v = j;
mi = dis[j];
}
vis[v] = true;
for(int j = 0; j < n; j++)
if(!vis[j] && dis[j] > map[v][j])
dis[j] = map[v][j];
}
for(int i = 1; i < n; i++)
dis[0] += dis[i]; //统计生成树长度
}
次小生成树例题
POJ-1679
题意
套用kuangbin的次小生成树模板。
只要找到次小生成树,然后和最小生成树比较一下即可。
代码
/******************************
*File name: poj1679.cpp
*Author: wzhzzmzzy
*Created Time: 五 4/21 17:18:59 2017
*TODO: POJ 1679 次小生成树
******************************/
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 105;
bool vis[maxn], used[maxn][maxn];
int lowc[maxn], pre[maxn], Max[maxn][maxn];
int prim(int cost[][maxn], int n){
int ans = 0;
memset(Max, 0, sizeof Max);
memset(used, false, sizeof used);
memset(vis, false, sizeof vis);
vis[0] = true;
pre[0] = -1;
for (int i = 1; i < n; ++i){
lowc[i] = cost[0][i];
pre[i] = 0;
}
lowc[0] = 0;
for (int i = 1; i < n; ++i){
int minc = inf;
int p = -1;
for (int j = 0; j < n; ++j) if (!vis[j] && minc > lowc[j]){
minc = lowc[j];
p = j;
}
if (minc == inf) return -1;
ans += minc;
vis[p] = true;
used[p][pre[p]] = used[pre[p]][p] = true;
for (int j = 0; j < n; ++j){
if (vis[j]) Max[j][p] = Max[p][j] = max(Max[j][pre[p]], lowc[p]);
if (!vis[j] && lowc[j] > cost[p][j]){
lowc[j] = cost[p][j];
pre[j] = p;
}
}
}
return ans;
}
int ans;
int check(int cost[][maxn], int n){
int Min = inf;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
if (cost[i][j] != inf && !used[i][j])
Min = min(Min, ans + cost[i][j] - Max[i][j]);
if (Min == inf) return -1;
return Min;
}
void solve(){
int cost[maxn][maxn], n, m;
scanf("%d%d", &n, &m);
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j){
if (i == j) cost[i][j] = 0;
else cost[i][j] = inf;
}
while (m--){
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
--x, --y;
cost[x][y] = cost[y][x] = w;
}
ans = prim(cost, n);
if (ans == -1 || ans == check(cost, n)) printf("Not Unique!\n");
else printf("%d\n", ans);
}
int main(){
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
并查集
一般并查集
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void join(int x, int y) {
int fx = find(x), fy = find(y);
if (fx != fy) fa[fx] = fy;
}
带权并查集
而带权并查集,其中的权需要转化成子节点与父节点之间的联系。这样向上查找时就能发现父节点和子节点之间的关系,以此来进行计算。
带权并查集的压缩路径方法是,在递归向上查找的同时,因为递归是直接到达最深处然后向上回溯的,所以只需要对每个点都做一次累加,这样回到原来的位置时就是全部的累加。合并树操作各有不同,主要是创建父节点的操作。
举个例子,虫子的一生( POJ - 2492 )中用权数组表示两个虫子的性别关系,更新时就只要考虑一下同性还是异性即可。
并查集例题
题解
并查集+离散化。
有一个01串,1e9位。给出一些子串,和他们中有奇数个还是偶数个1,求这些中有几个是对的。
因为长度太长所以开不下这么多数组,而子串只有五千个,所以需要哈希一下。只要存最多一万个数字就可以。为了避免不在一起的相邻所以多存一位,就是两万位。然后用一个数组存当前下标之前有奇数还是偶数个1,最后用并查集求解。
代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 100005;
int fa[maxn], val[maxn], n, m;
int hashSet[maxn];
struct {
int u, v, w;
}node[maxn];
int find(int n){
int k = fa[n];
if(fa[n] != n){
fa[n] = find(fa[n]);
val[n] = (val[n] + val[k])%2;
}
return fa[n];
}
void init(){
for (int i = 0; i <= n; ++i)
val[i] = 0, fa[i] = i;
}
int main(){
while (~scanf("%d", &n)){
int i, k = 0;
//init放在这里会RE
scanf("%d", &m);
for (i = 0; i < m; ++i){
char s[5];
scanf("%d%d%s", &node[i].u, &node[i].v, s);
node[i].w = s[0] == 'e'? 0:1;
hashSet[k++] = node[i].u - 1;
hashSet[k++] = node[i].u;
hashSet[k++] = node[i].v - 1;
hashSet[k++] = node[i].v;
}
hashSet[k++] = n;
hashSet[k++] = n - 1;
sort(hashSet, hashSet+k);
n = (int)(unique(hashSet, hashSet+k) - hashSet);
init();
//init放这里就AC
for (i = 0; i < m; ++i){
int u = (int)(lower_bound(hashSet, hashSet+n, node[i].u-1) - hashSet);
int v = (int)(lower_bound(hashSet, hashSet+n, node[i].v) - hashSet);
int fu = find(u), fv = find(v);
if (fu == fv && (val[u] + node[i].w)%2 != val[v])
break;
if (fu < fv){
fa[fv] = fu;
val[fv] = (val[u] + node[i].w - val[v] + 2) % 2;
}
if (fu > fv){
fa[fu] = fv;
val[fu] = (val[v] - node[i].w - val[u] + 2) % 2;
}
}
printf("%d\n", i);
}
return 0;
}
线段树
漂浮法
#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <queue>
#include <string>
using namespace std;
int t, n, a, b;
#define maxn 10001
int l[maxn], l[maxn];
int ans[maxn];
void cover(int a, int b, int to, int id) {
while (to <= n && (b < l[to] || a > r[to]))
to++;
if (to == n+1) {
ans[id] = b - a + 1; return;
}
if (a < l[to])
cover(a, l[to] - 1, to + 1, id);
if (b > r[to])
cover(r[to] + 1, b, to + 1, id);
}
int main() {
scanf("%d", &t);
while (t--) {
memset(ans, 0, sizeof(ans));
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d %d", &l[i], &r[i]);
for (int i = n; i >= 1; i--)
cover(l[i], r[i], i + 1, i);
int cnt = 0;
for (int i = 1; i <= n; i++)
if (ans[i])cnt++;
printf("%d\n", cnt);
}
}
线段树例题
POJ-3468
代码
#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <queue>
#include <string>
using namespace std;
#define maxn 100010<<2
#define maxm 1000000000
int n, m;
long long A[100010];
long long Sum[maxn];
long long Add[maxn];
void pushup(int rt) {
Sum[rt] = Sum[rt << 1] + Sum[rt << 1 | 1];
}
void build(int l,int r,int rt) {
if (l == r) { Sum[rt] = A[l]; return; }
int m = (l + r) >> 1;
build(l, m, rt << 1);
build(m + 1, r, rt << 1 | 1);
pushup(rt);
}
void pushdown(int rt,int ln,int rn) {
if (Add[rt]) {
Add[rt << 1] += Add[rt];
Add[rt << 1 | 1] += Add[rt];
Sum[rt << 1] += Add[rt] * ln;
Sum[rt << 1 | 1] += Add[rt] * rn;
Add[rt] = 0;
}
}
void update(int a, int b, int c, int l, int r, int rt) {
if (a <= l&&b >= r) {
Sum[rt] += c*(r - l + 1);
Add[rt] += c;//本区间正确,子区间Sum仍然要用Add增加
return;
}
int m = (l + r) >> 1;
pushdown(rt, m - l + 1, r - m);
if (a <= m)update(a, b, c, l, m, rt << 1);
if (b > m)update(a, b, c, m + 1, r, rt << 1 | 1);
pushup(rt);
}
long long query(int a, int b, int l, int r, int rt) {
if (a <= l&&b >= r) { return Sum[rt]; }
int m = (l + r) >> 1;
pushdown(rt, m - l + 1, r - m);
long long ans = 0;
if (a <= m)ans += query(a, b, l, m, rt << 1);
if (b > m)ans += query(a, b, m + 1, r, rt << 1 | 1);
return ans;
}
int main() {
while (~scanf("%d %d", &n, &m)) {
memset(Add, 0, sizeof(Add));
for (int i = 1; i <= n; i++) {
scanf("%lld", &A[i]);
}
build(1, n, 1);
char s[10];
int a, b, c;
for (int i = 1; i <= m; i++) {
scanf("%s", s);
if (s[0] == 'Q') {
scanf("%d %d", &a, &b);
printf("%lld\n",query(a, b, 1, n, 1));
}
else {
scanf("%d %d %d", &a, &b, &c);
update(a, b, c, 1, n, 1);
}
}
}
}
动态规划
最长公共子序列
if (a[i] == b[j])
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
最长非降子序列(朴素)
dp[i]
存储a[i]
位置及之前最长非降子序列长度。
for (int i = 1; i <= n; ++i)
for (int j = 1; j < i; ++j)
if (a[i] >= a[j])
dp[i] = max(dp[i], dp[j]+1);
最长非降子序列(二分)
for (int i = 1; i <= n; ++i){
int k = upper_bound(a[i]);
b[k+1] = a[i];
}
最大字段和
int f(int n){
bool flag = false;
for (int i = 1; i <= n; ++i){
if (!flag && a[i] > 0) flag = true;
if (dp[i-1] + a[i] < 0) dp[i] = 0;
else dp[i] = dp[i-1] + a[i];
}
if (!flag){
int ans = 0;
for (int i = 1; i <= n; ++i)
ans += a[i];
return ans;
}
else return dp[n];
}
递推例题
POJ - 1661
题意
Jimmy从坐标为(x,y)
的点掉落下来,速度1单位/s
,如果掉落下去的距离超过了max
就会摔死。空间中有许多横着的木板,属性为(x1,x2,h)
。在木板上可以横向移动,速度1单位/s
。求最快到达地面的时间。
题解
基础DP。
用dp[i][0]
表示从第i
块木板左侧掉落的到达地面时间,dp[i][1]
表示右侧。转移方程为:
dp[i][0] =
p[i].h - p[k].h +
min(dp[k][0]+p[i].x1-p[k].x1,
dp[k][1]+p[k].x2-p[i].x1);
dp[i][1] =
p[i].h - p[k].h +
min(dp[k][0]+p[i].x2-p[k].x1,
dp[k][1]+p[k].x2-p[i].x2);
代码
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <iostream>
using namespace std;
struct plat{
int x1, x2, h;
}p[1005];
int maxn, dp[1005][2];
bool cmp(plat a, plat b){
return a.h < b.h;
}
void calc_left(int i){
int k = i - 1;
while (k > 0 && p[i].h - p[k].h <= maxn){
if (p[i].x1 >= p[k].x1 && p[i].x1 <= p[k].x2){
dp[i][0] = p[i].h - p[k].h + min(dp[k][0]+p[i].x1-p[k].x1,
dp[k][1]+p[k].x2-p[i].x1);
return;
} else --k;
}
if (p[i].h - p[k].h > maxn)
dp[i][0] = 0x3f3f3f3f;
else
dp[i][0] = p[i].h;
}
void calc_right(int i){
int k = i - 1;
while (k > 0 && p[i].h - p[k].h <= maxn){
if (p[i].x2 >= p[k].x1 && p[i].x2 <= p[k].x2){
dp[i][1] = p[i].h - p[k].h + min(dp[k][0]+p[i].x2-p[k].x1,
dp[k][1]+p[k].x2-p[i].x2);
return;
} else --k;
}
if (p[i].h - p[k].h > maxn)
dp[i][1] = 0x3f3f3f3f;
else
dp[i][1] = p[i].h;
}
void solve(){
memset(dp, 0, sizeof dp);
int n;
scanf("%d%d%d%d", &n, &p[0].x1, &p[0].h, &maxn);
++n, p[0].x2 = p[0].x1;
for (int i = 1; i < n; ++i)
scanf("%d%d%d", &p[i].x1, &p[i].x2, &p[i].h);
p[n].x1 = -20001, p[n].x2 = 20001, p[n].h = 0;
sort(p, p+n+1, cmp);
for (int i = 1; i <= n; ++i){
calc_left(i);
calc_right(i);
}
printf("%d\n", min(dp[n][0], dp[n][1]));
}
int main(){
int t;
scanf("%d", &t);
while (t--) solve();
return 0;
}
01背包
每个物品只能放入一次。
思路
用f[i][v]
表示,第i
个大小为v
的物品放入时的总价值。
c[i]
表示第i
个物品的价值。w[i]
为第i
个物品的大小。
状态转移方程:f[i][v] = max(f[i-1][v], f[i-1][v-w[i]]+c[i]);
状态转移方程表示,取放入或者不放入第i
个物品两种情况的最大值。
空间优化(滚动数组)
初始状态方程的空间复杂度是O(V*W)
,可以进一步优化。
可以将空间优化为O(2*W)
,即纵向大小为2。
for(i=1; i<=N; i++){
for(j=t[i]; j<=V; j++)
f[t^1][j] = max(f[c][j-w[i]]+c[i], f[t][j]);
t ^= 1;
}
异或滚动可以在0和1之间切换,可以利用上下反复更新。
空间优化(一维数组)
既然可以用两行进行更新,那为什么不能用一行。
观察问题,两行更新时,用上一行的前部分更新下一行的后部分。
所以单行更新时要从后往前遍历,这样可以用前面的更新后面的。
for(i=1; i<=N; i++)
for(j=V; j>=w[i]; j--)
f[j] = max(f[j-w[i]]+c[i], f[j]);
这样就可以用一维数组来进行更新。
可以写成函数,封装起来。
void ZeroOnePack(int cost, int weight){
for(int i=V; i>=weight; i++)
f[i] = max(f[i], f[i-weight]+cost)
}
初始化的细节问题
一般问题会有两种问法:
- 刚好装满背包
- 不用装满背包
如果是第一种,f[0]=0,f[1]……f[N]=INF;
如果是第二种,f[0]……f[N]=INF;
理解:
如果是第一种,初始状态只有0符合理想状态,只有0才能被空“装满”。
如果是第二种,所有都符合理想状态。
完全背包问题
和01背包相似,所不同的是可取的物品数是无限。
前置小优化
对于i``j
两个物品,如果c[i]>c[j] && w[i]<w[j]
,就舍去i
物品。
另外,针对背包问题而言,比较不错的一种方法是:首先将重量大于V的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(V+N)地完成这个优化。
基本思路
状态转移方程f[i][v]=max{f[i-1][v-k*w[i]]+k*c[i]},(0<=k*w[i]<=V)
转化为01背包求解
一件物品最多只能放V/c[i]
件,所以可以把一件物品,看成V/c[i]
件物品,作为01背包解答。
另一种更好的办法是把第i
种物品拆成大小为w[i]*2^k
、价值为c[i]*2^k
的若干件物品,其中k
满足w[i]*2^k<=V
。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(log(V/w[i]))
件物品,是一个很大的改进。
O(VN)
算法
for(int i=1; i<=N; i++)
for(int j=w[i]; j<=V; j++)
f[j] = max{f[v], f[v-w[i]]+c[i]};
这个算法和之前的01背包相比只是第二层的遍历方向改变了。因为01背包要保证每个物品只能选择一次,但是完全背包不必,所以改变遍历方向就可以得到结果。
这个算法也可以从另外的思路中得出,
例如,基本思路中的公式可以化作这个形式:
f[i][v]=max(f[i-1][v], f[i][v-w[i]]+c[i]);
用函数封装:
void CompletePack(int cost, int weight){
for(int i=weight; i<=V; i++)
f[i] = max(f[i], f[i-weight]+cost);
}
多重背包问题
每件物品数量不一定为1但有限。
基本思路
问题和完全背包很相似。
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[I]}(0<=k<=n[I])
复杂度为O(V*Σn[i])
。
转化为01背包问题
用n[i]
存储,可以将每种物品转化为n[i]
件物品,然后用01背包方案求解。复杂度不变。
如果要进行优化的话,依然用二进制思想,同上。
这样可以将时间优化为O(V*Σlog n[i])
。
void MultiplePack(int weight, int cost, int amount){
if(cost * amount >= V){
CompletePack(cost, weight);
return;
}
int k = 1;
while(k < num){// num 为物品种数
ZeroOnePack(k*cost, k*weight);
amount = amount-k;
k *= 2;
}
ZeroOnePack(amount*cost, amount*weight);
}
数位DP例题
HDU - 2089
题意
求从l
到r
的所有数字中,不含有4
和62
的数字有多少。
题解
数位DP入门。
用dp[i][j]
表示j
开头的i
位数。首先打表,然后根据读入的数字挨个匹配累加即可。
递推公式:
for (int i = 1; i < 7; ++i)
for (int j = 0; j < 10; ++j)
for (int k = 0; k < 10; ++k)
if (j != 4 && !(j == 6 && k == 2))
dp[i][j] += dp[i-1][k];
代码
/******************************
*File name: hdu2089.cpp
*Author: wzhzzmzzy
*Created Time: 二 4/25 20:05:44 2017
*TODO: HDU 2089 数位DP
******************************/
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>
using namespace std;
int dp[8][10];
void init(){
memset(dp, 0, sizeof dp);
dp[0][0] = 1;
for (int i = 1; i < 7; ++i)
for (int j = 0; j < 10; ++j)
for (int k = 0; k < 10; ++k)
if (j != 4 && !(j == 6 && k == 2))
dp[i][j] += dp[i-1][k];
}
int calc(int x){
int digit[8], len = 0, ans = 0;
while (x > 0){
digit[++len] = x%10;
x /= 10;
}
digit[len+1] = 0;
for (int i = len; i; --i){
for (int j = 0; j < digit[i]; ++j)
if (j != 4 && !(digit[i+1]==6&&j==2))
ans += dp[i][j];
if (digit[i] == 4||(digit[i]==2&&digit[i+1]==6))
break;
}
return ans;
}
int main(){
int n, m;
init();
while (~scanf("%d%d", &n, &m) && n && m){
printf("%d\n", calc(m+1)-calc(n));
}
}
状压DP
HDU - 1074
题意
给出n
门作业的名称、截止时间和所需时间。当超过截止时间时会扣分。求最小的扣分数和做作业的顺序。
题解
状态压缩DP。
看了题解才知道这样做。
首先是二进制法遍历集合全排列,然后遍历当前排列中包含的上一个做完的作业,即遍历每一种作业,找出扣分的最小值作为上一个做完的作业。记录这一状态,并且记录前驱(输出用)。
代码
/******************************
*File name: hdu1029.cpp
*Author: wzhzzmzzy
*Created Time: 日 4/23 20:01:35 2017
*TODO: HDU 1029 状压DP 水
******************************/
#include <cstdio>
#include <cstring>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <queue>
#include <set>
#include <map>
using namespace std;
char name[16][200];
int t[1<<15], deadline[16], finish[16];
int pre[1<<15], dp[1<<15];
void output(int m){
if (!m) return;
output(m-(1<<pre[m]));
printf("%s\n", name[pre[m]]);
}
int main(){
int T;
scanf("%d", &T);
while (T--){
int n;
scanf("%d", &n);
int maxn = 1<<n;
for (int i = 0; i < n; ++i)
scanf("%s%d%d", name[i], deadline+i, finish+i);
for (int i = 1; i < maxn; ++i){
dp[i] = 0x3f3f3f3f;
for (int j = n-1; j >= 0; --j){
int te = 1<<j;
if (!(i&te)) continue;
int score = t[i-te]-deadline[j]+finish[j];
if (score < 0) score = 0;
if (dp[i] > dp[i-te]+score){
dp[i] = dp[i-te]+score;
t[i] = t[i-te]+finish[j];
pre[i] = j;
}
}
}
printf("%d\n", dp[maxn-1]);
output(maxn-1);
}
return 0;
}
树形DP
HDU - 4044
代码
#include <stdio.h>
#include <iostream>
#include <string.h>
#include <algorithm>
#include <queue>
#include <string>
#include <vector>
#include <set>
#include <math.h>
#include <map>
#define ull unsigned long long
#define ll long long
#define mp map
#define FOR(a,b) for(int i=a;i<=b;i++)
#define ls l,m,rt<<1
#define rs m+1,r,rt<<1|1
using namespace std;
int m;
const int maxm = 210;
const int maxn = 1010;
vector<int>aaa[maxn];
int weaponChoice[maxn];
int cost[maxn][52];
int power[maxn][52];
int dp[maxn][maxm]; //dp[i][j]:i与其子树消耗j资源的最薄弱链最大值
void dfs(int u,int fa){
for(int i=m;i>=0;i--){
for(int j=1;j<=weaponChoice[u];j++){
if(cost[u][j]<=i)dp[u][i]=max(dp[u][i],power[u][j]);
}
}
if(aaa[u].size()==1&&u!=1) return;
int maxson[maxn]; //maxson[i]:u子树花费资源i时最大值
memset(maxson,0x3f3f3f3f,sizeof(maxson));
for(int e=0;e<aaa[u].size();e++){//枚举u的子节点
int v=aaa[u][e];
if(v==fa)continue;
dfs(v,u); //计算v树的最优解,dp[v]系
for(int i=m;i>=0;i--){ //枚举给u子树分配的资源
int maxx=0;
for(int j=0;j<=i;j++){
maxx=max(maxx,min(maxson[i-j],dp[v][j])); //其他子树分i-j,v树分j
}
maxson[i]=maxx;
}
}
for(int i=m;i>=0;--i)
for(int k=0;k<=i;++k)
dp[u][i]=max(dp[u][i],dp[u][i-k]+maxson[k]);
}
int main(){
int tcase;
scanf("%d",&tcase);
while(tcase--){
memset(dp,0,sizeof(dp));
int n;
scanf("%d",&n);
for(int i=0;i<=n;i++)aaa[i].clear();
int a,b;
for(int i=1;i<n;i++){
scanf("%d %d",&a,&b);
aaa[a].push_back(b);
aaa[b].push_back(a);
}
scanf("%d",&m);
for(int i=1;i<=n;i++){
scanf("%d",&weaponChoice[i]);
for(int j=1;j<=weaponChoice[i];j++){
scanf("%d %d",&a,&b);
cost[i][j]=a;power[i][j]=b;
}
}
dfs(1,-1);
printf("%d\n",dp[1][m]);
}
}
数学
GCD与LCM
-
GCD
计算方式来自公式gcd(a,b)=gcd(b,a%b)
int gcd(int a, int b){
return b>0? b : a%b;
}
-
LCM
计算方式来自性质ab=gcd(a,b)*lcm(a,b)
int lcm(int a, int b){
return a * b / std::__gcd(a, b);
}
- 还有一个性质
lcm(m*a,m*b)=m*gcd(a,b)
简易扩展欧几里得算法
void gcd(int a, int b, int& d, int& x, int& y) {
if (!b) { d = a; x = 1; y = 0; }
else { gcd(b, a%b, d, y, x); y -= x*(a / b); }
}
杂项模板
万金油小算法
快读
通过getchar
和强制类型转换加快读取整形数字的速度。
int Input(){
char c;
for (c = getchar(); c<'0' || c>'9'; c = getchar());
int a = c - '0';
for (c = getchar(); c >= '0' && c <= '9'; c = getchar())
a = a*10 + c - '0';
return a;
}
交换
指针交换变量值。
void swap(int* a, int* b){
int t = *a; *a = *b; *b = t;
}
前驱路径输出
递归回溯输出pre
数组中的路径。
void output(int cur, int pre[]){
if (cur == 0) return;
output(pre[cur], pre);
printf("%d\n", cur);
}
矩阵快速幂
#include <cstdio>
#include <algorithm>
#include <string.h>
#include <iostream>
using namespace std;
#define maxn 101 //题目极限尺寸
int n; //实际矩阵尺寸
struct Matrix {
long long mat[maxn][maxn];
};
Matrix operator * (Matrix a, Matrix b) {
Matrix c;
memset(c.mat, 0, sizeof(c.mat));
int i, j, k;
for (k = 0; k < n; ++k) {
for (i = 0; i < n; ++i) {
for (j = 0; j < n; ++j) {
c.mat[i][j] += a.mat[i][k] * b.mat[k][j];
c.mat[i][j] %= 10000;
}
}
}
return c;
}
Matrix operator ^ (Matrix a, int k) {
Matrix c;
int i, j;
for (i = 0; i < n; ++i)
for (j = 0; j < n; ++j)
c.mat[i][j] = (i == j); //初始化为单位矩阵
for (; k; k >>= 1) {
if (k & 1) c = c*a;
a = a*a;
}
return c;
}
int main() {
int t;
while (scanf("%d", &t) && t != -1) {
Matrix fib;
n = 2;
fib.mat[0][0] = fib.mat[0][1] = fib.mat[1][0] = 1;
fib.mat[1][1] = 0;
fib = fib^t;
cout << fib.mat[0][1]%10000 << endl;
}
}
STL用法
struct 重载运算符、构造函数
struct node{
int d,u;
friend bool operator<(node a, node b){
return a.d > b.d;
}
node():{}
node(int dist, int point): d(dist), u(point){}
};
std::sort()
按照a
字段降序排序,a
相同时按b
降序。
bool cmp(node a, node b){
if (a.a == b.a)
return a.b > b.b;
else
return a.a > b.a;
}
std::map
map<string,int> a;
a["hello"] = 1;
a["world"] = 2;
for (map<string,int>::iterator i = a.begin(); i != a.end(); ++i)
cout << i->first << " " << i->second << endl;
a.count("world") == 2; // Ture;
auto 变量
C++11,auto
为一块指向无类型内存的变量。
int a = 0;
auto it = &a;
cout << *it << endl;
BigInteger
import java.math.BigInteger;
import java.util.*;
public class test {
public static void main(String args[]){
BigInteger
a = new BigInteger("32"),
m = new BigInteger("12");
BigDecimal b = new BigDecimal("1.0");
a = a.pow(1);
a = a.add(a);
a = a.divide(a);
a = a.mod(a);
a = a.abs();
a = a.gcd(a);
a = a.modPow(a, m);
System.out.print(a.toString()+"\n");
}
}
STL去重
sort(xs.begin(),sx.end());
xs.erase(unique(xs.begin(),xs.end()),xs.end()); //unique将同样的元素排至vector末位,返回首个重复元素地址
STL算法离散化
思路:先排序,再删除重复元素,然后就是索引元素离散化后对应的值。
假定待离散化的序列为a[n],b[n]是序列a[n]的一个副本,则对应以上三步为:
sort(sub_a,sub_a+n);
int size=unique(sub_a,sub_a+n)-sub_a;//size为离散化后元素个数
for(i=0;i<n;i++)
a[i]=lower_bound(sub_a,sub_a+size,a[i])-sub_a + 1;//k为b[i]经离散化后对应的值
对于第3步,若离散化后序列为0, 1, 2, ..., size - 1则用lower_bound,从1, 2, 3, ..., size则用upper_bound,其中lower_bound返回第1个不小于b[i]的值的指针。
而upper_bound返回第1个大于b[i]的值的指针,当然在这个题中也可以用lower_bound然后再加1得到与upper_bound相同结果,两者都是针对以排好序列。使用STL离散化大大减少了代码量且结构相当清晰。
其他
Vim 配置
set nu
set mouse=a
set cursorline
set confirm
set tabstop=4
set smarttab
set shiftwidth=4
set smartindent
set cindent
set nobackup
set matchtime=1
set showmatch
set setnocompatible
colo evening
syntax on
syntax enable
G++ 运行
g++ a.cpp
./a.out