输入
- 输入完毕先按enter,再按ctrl+z,最后按enter,即可结束输入。
int x;
while(scanf("%d",&x)==1)
{
//程序
}
- 使用文件输入:
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
预编译
#define LOCAL
int main()
{
#ifdef LOCAL
freopen("input.txt","r",stdin);
freopen("output.txt","w",stdout);
#endif
//程序
}
类型推导
vector<int> vec;
vector<int>::iterator cit = vec.begin();
在C++11中可以写成:
vector<int> vec;
auto cit=vec.begin();
容器的for循环遍历
for(const auto&p: vec)
cout<<p<<endl;
初始化容器
C++98中需要一个个push_back,C++11中初始化容器:
vector<string> vec{1,2,3};
map<string,string> dict{{"ABC","123"},{"BCD","234"}};
哈希容器
当不需要元素排序时,可以尽量使用容器来获得更好的查找性能:
#include<unordered_map>
unordered_map<string,int> um{
{"A",1972},{"B",1976},
{"C",1967},{"D",1968}
};
um["E"]=1983;//添加
for(auto x:um)//迭代输出
cout<<x.first<<x.second;
指针参数
正确:
#include<stdio.h>
void swap(int* a,int* b)
{
int t=*a;*a=*b;*b=t;
}
int main()
{
int a=3,b=4;
swap(&a,&b);
return 0;
}
数组参数
错误:sizeof(a)无法得到数组大小,传入的是地址
int sum(int a[])
{
int ans=0;
for(int i=0;i<sizeof(a);i++)
ans+=a[i];
return ans;
}
正确:
int sum(int* a,int n)
{
int ans=0;
for(int i=0;i<n;i++)
ans+=a[i];
return ans;
}
sum(a+1,3);//a[1]+a[2]+a[3]
改进:
int sum(int *begin,int* end)
{
int n=end-begin;
int ans=0;
for(int i=0;i<n;i++)\
ans+=begin[i];
return ans;
}
sum(a+1,a+3);//a[1]+a[2]+a[3]
数组和字符串
- memcpy(b,a,sizeof(int)*k);//从a复制k个元素到b
- memset(a,0,sizeof(a));//把数组a清零
STL框架
const声明
使用const声明的数组长度,代替define。
行输入
虽然string和sstream都很方便,但string很慢,sstream更慢,谨慎使用。
#include<iostream>
#include<string>
#include<sstream>
using namespace std;
int main()
{
string line;
while(getline(cin,line)){
int sum=0,x;
stringstream ss(line);
while(ss>>x)
sum+=x;
cout<<sum<<"\n";
}
return 0;
}
结构体
struct Point{
int x,y;
Point(int x=0,int y=0):x(x),y(y){}//构造函数
//Point(int x=0,int y=0){this->x=x;this->y=y;}
}
Point operator +(const Point& A,const Point& B){
return Point(A.x+B.x,A.y+B.y);
}
模板
int sum(int* begin,int* end){}
只能求int
的和,不能求double
的和。
template<typename T>
T sum(T* begin,T* end){
T* p=begin;
T ans=0;
for(T* p=begin;p!=end;p++)
ans=ans+*p;
return ans;
}
暴力求解
消防车问题:输入一个n(n<=20)个结点的无向图以及某个结点k,按照字典序从小到大顺序输出从结点1到结点k的所有路径,要求结点不能重复经过。
- 使用并查集判断1和k是否连通
- DFS搜索所1->k所有路径
- 对所经过的结点进行判重
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<set>
#include<vector>
#include<sstream>
//循环宏定义
#define _for(i,a,b) for(int i=(a);i<(b);++i)
using namespace std;
const int MAXN = 20 + 4;
int N, pa[MAXN];
set<int> G[MAXN];
int find_pa(int x) {
return pa[x] == x ? x : (pa[x] = find_pa(pa[x]));
}
ostream& operator<<(ostream& os, const vector<int>& s) {
bool first = true;
for (const auto x : s) {
if (first)
first = false;
else
os << ' ';
os << x;
}
return os;
}
//搜索src->dest所有路径
void dfs(int src, int dest, vector<int>& path, vector<string>&paths) {
path.push_back(src);
if (src == dest) {
//搜到目标
stringstream os;
os << path;
paths.push_back(os.str());
}
else
{
for (auto v : G[src]) {
if (find(path.begin(), path.end(), v) != path.end())
continue;//走出的路径上已经有v存在
dfs(v, dest, path, paths);
}
}
path.pop_back();
}
int main() {
for (int kase = 1, from, to; scanf("%d", &N) == 1; kase++) {
_for(i, 0, MAXN) G[i].clear(), pa[i] = i;
while (true)
{
scanf("%d%d", &from, &to);
if(from==0||to==0)
break;
G[from].insert(to), G[to].insert(from);
int pf = find_pa(from), pt = find_pa(to);
if (pf != pt)
pa[pt] = pf;
}
vector<string> paths;
vector<int> path;
if (find_pa(1) == find_pa(N))
dfs(1, N, path, paths);
printf("CASE %d:\n", kase);
for (auto& p : paths)
puts(p.c_str());
printf("There are %lu routes from the firestation to streercornet %d.\n", paths.size(), N);
}
system("pause");
return 0;
}
背包问题(贪心法)
最优装载问题
给出n个物体,第i个物体重量为wi,选择尽量多的物体,使得总重量不超过C。
由于只关心物体的数量,所以装重的没有轻的划算,只需把所有物体的重量按照从小到大的顺序排序,依次选择每个物体,直到装不下为止。
部分背包问题
有n个物体,第i个物体的重量为wi,价值为vi,在总重量不超过C的情况下让总价值尽量高。每个物体都可以只取走一部分,价值和重量按比例计算。
一种直观的贪心策略是:优先拿“价值除以重量的值最大的”,直到重量和正好为C。
乘船问题
有n个人,第i个人重量为wi。每艘船的最大载重量均为C,且最多只能乘两个人。用最少的船装载所有人。
如果每个人都无法和他一起坐船,则他一个人一个船。否则,选择能和他一起坐船的选最重的人。
动态规划
- 状态和状态转移方程
- 最优子结构和重叠子问题
- 递推法和记忆化搜索
- DAG上的动态规划
- 两种状态定义方法和刷表法
动态规划的核心是:状态
和状态转移方程
。
路径规划问题
从第一行的数开始,每次可以往下或右下走一格,直到走到最下行,把沿途经过的数全加起来,如何走才能使得和尽量大?
如果用回溯法(剪枝)
,求出所有可能的路线,从中选出最优路线,此方法效率太低。
可以设值为
,解为
,
为左下最优解,
为右下最优解,则最优解为:
即寻求最优子结构
,构建状态方程
。
记忆化搜索与递归
有了状态方程之后,可以采用如下方法计算。
递归计算
,有重复计算,效率低,如图a[3,2]分支被计算两次,存在重叠子问题
。
int solve(int i,int j){
return a[i][j] + (i == n ? 0 : max(solve(i+1,j),solve(i+1,j+1)));
}
递推计算
,注意边界处理
int i,j;
for(j=1;j<=n;j++)
d[n][j]=a[n][j];
for(i=n-1;i>=1;i--)
for(j=1;j<=i;j++)
d[i][j] = a[i][j] + max(d[i+1][j],d[i+1][j+1]);
记忆化搜索
,用memset(d,-1,sizeof(d))把d全部初始化为-1
,然后编写递归函数:
int solve(int i,int j){
if(d[i][j] >= 0)
return d[i][j];
return d[i][j]=a[i][j]+(i==n?0:max(solve(i+1,j),solve(i+1,j+1)));
}
程序依然是递归
,但d初始化为-1,计算过后为非负
,所以可以通过判断d非负否避免不必要的重复计算。
有向无环图(DAG)动态规划
有向无环图的动态规划是学习动态规划的基础,很多问题都可以转化为DAG上的最长路、最短路或路径计数问题。
嵌套矩形问题
有n个矩形,每个矩形都可以用两个整数a、b进行描述,表示它的长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中,当且仅当a<c,b<d,或者b<c,a<d。例如,(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)内。你的任务是选出尽量多的矩形排成一行,使得除了最后一个之外,每一个矩形都可以嵌套在下一个矩形内。如果有多解,应使序号尽量小。
可以用图
来建模,X可以嵌套在Y里,就从X到Y连接一条有向边。这个有向图是无环
的,因为一个矩形无法直接或间接地嵌套在自己内部。无初始状态
,求DAG上的最长路径
。
- 由于没有规定起点,设从i出发,第一步只能走相邻点 ,记忆化搜索:
int dp(int i)
{
int& ans=d[i];//声明一个对d[i]的引用ans
if(ans>0)
return ans;
ans=1;
for(int j=1;j<=n;j++)
if(G[i][j])
ans=max(ans,dp(j)+1);
return ans;
}
- 选择最小的序号: ,选择最小的j:
void print_ans(int i)
{
printf("%d ",i);
for(int j=1;j<=n;j++)
if(G[i][j] && d[i]==d[j]+1)
print_ans(j);
break;
}
硬币问题
有n种硬币,面值分别为V1.V2,…,Vn,每种都有无限多。给定非负整数S,可以选择多少个硬币,使得面值恰好为S?输出硬币数目的最大值和最小值。1<=n<=100,0<=S<=10000,1<=Vi<=S。初始状态
为S,目标状态为0。
- d(i)的确切含义为从结点i出发到结点0的最长路径长度。
int dp(int S){
int& ans=d[S];
if(ans != -1)//-1表示没有算过
return ans;
ans=-(1<<30);//-2^30表示无法到达
for(int i=1;i<=n;i++)
if(S>+V[i])
ans=max(ans,dp(S-V[i])+1);
return ans;
}
路径的长度可能为0,所以用-1表示“没有访问过”
,初始化使用memset(d,-1,sizeof(d)),所以把if(ans>0)改成了if(ans!=-1),-2^30表示无法到达
。
- 要求最小、最大值,记忆化搜索就必须写两个。在这种情况下,用递推更加方便:
minv[0]=maxv[0]=0;
for(int i=1;i<=S;i++){
minv[i]=INF;
maxv[i]=-INF;
}
for(int i=1;i<=S;i++)
for(int j=1;j<=n;j++){
minv[i]=min(minv[i],minv[i-V[j]]+1);
maxv[i]=max(maxv[i],maxv[i-V[j]]+1);
}
printf("%d %d\n",minv[S],maxv[S]);
- 输出字典序的最小方案,调用print_ans(min,S),最大为print_ans(max,S)
void print_ans(int* d,int S){
for(int i=1;i<=n;i++)
if(S>=V[i]&&d[S]==d[S-V[i]]+1){
printf("%d ",i);
printf_ans(d,S-V[i]);
break;
}
}
- 另一种打印路径的方法:在递推时直接用min_coin[S]记录满足min[S]==min[S-V[i]+1]的最小的i,则打印路径时可以省去print_ans中的循环,使用
空间换时间
。
for(int i=1;i<=S;i++)
for(int j=1;j<=n;j++)
if(i>=V[j]){
if(min[i]>min[i-V[j]]+1){
min[i]=min[i-V[j]]+1;
min_coin[i]=j;
}
if(max[i]<max[i-V[j]]+1){
max[i]=max[i-V[j]]+1;
max_coin[i]=j;
}
}
void print_ans(int*d,int S){
while(S){
printf("%d",d[S]);
S -= V[d[S]];
}
}
0-1背包问题
物品无限的背包问题
有n种物品,每种都有无穷多个,第i种物品的体积为Vi,重量为Wi。选一些物品装到一个容器为C的背包中,使得背包内的物品在总体积不超过C的前提下重量尽可能大。 。
分析可知,是在有向无环图的基础上增加了权值,+1变成了+W[i]。
0-1背包问题
当物体只有一个时,称为0-1背包,仅仅考虑体积,无法得知每个物品是否已经用过。需要对问题进行分层
:用d(i,j)表示当前在第i层,背包剩余容量为j时接下来的最大重量和,则
,d(i,j)表示把第i,i+1,i+2…n个物品装到容量为j的背包中的最大总重量。分层后,采用递推法更好:
- 如下最终答案为d[1][C]:
for(int i=n;i>=1;i--)
for(int j=0;j<=C;j++){
d[i][j]=(i==n ? 0 : d[i+1][j]);
if(j>=V[i])
d[i][j]=max(d[i][j],d[i+1][j-V[i]]+W[i]);
}
- 还可采取另一种状态定义 ,最终答案为f(n,C):
for(int i=1;i<=n;i++)
for(int j=0;j<=C;j++){
f[i][j]=(i==1?0:f[i-1][j]);
if(j>=V[i])
f[i][j]=max(f[i][j],f[i-1][j-V[i]]+W[i]);
}
这种定义运行边读边计算:
for(int i=1;i<=n;i++){
scanf("%d%d",&V,&W);
for(int j=0;j<=C;j++){
f[i][j]=(i==1?0:f[i-1][j]);
if(j>=V)
f[i][j]=max(f[i][j],f[i-1][j-V]+W);
}
}
小记
- 用DFS求连通域
- 用BFS求最短路
- 拓扑排序:DFS求拓扑排序,成功则为有向无环,否则有环。
- vector保存结点避免溢出
- 回溯法(剪枝):无法继续扩展,阶数递归,返回上一层
for(...)
if(...)
OK=0;break;
if(OK)
dfs(递归);
- cstring的memcmp和memcpy复制比循环快
- 树、图的DFS、BFS判重技术:哈希技术,将结点映射为整数
- 图DFS检查idx,BFS检查d值是否为-1,以此来判断是否访问过
- scanf不安全问题
#define _CRT_SECURE_NO_WARINGS
- 消防车问题:
if(src==dest)
//找到目标
else
for(遍历下一个结点)
if(判重)
continue;
dfs(递归);