Dp从入门到experienced
一、入门篇
1、dp思维的培养
2、背包(0-1背包、完全背包、分组背包、多重背包)
3、LIS
4、LCS
二、进阶篇
1、区间dp
2、树形dp
3、数位dp
4、概率(期望) dp
5、状态压缩dp(TSP、插头dp)
6、数据结构优化的dp
后言:推荐的论文及博客
入门篇
1. dp思维的培养
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2084
AC代码:
#include<iostream>
using namespace std;
int max(int a,int b)
{
return a>b?a:b;
}
int main()
{
inti,j,c,n,dp[105][105];
cin>>c;
while(c--)
{
cin>>n;
for(i=0;i<n;i++)
{
for(j=0;j<=i;j++)
{
cin>>dp[i][j];
}
}
for(i=n-2;i>=0;i--)//从下面往上面加
{
for(j=0;j<=i;j++)
{
dp[i][j]+=max(dp[i+1][j],dp[i+1][j+1]);//假如说是dp[7][0],应该看dp[8][0]和dp[8][1]之间的大小
}
}
cout<<dp[0][0]<<endl;
}
return 0;
}
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2018
AC思路: dp【1】=1 ,dp【2】=2,dp【3】=3,dp【4】=4;dp【n】=dp【n-1】+dp【n-4】;
推荐:[高精度+递推] uva 10328 Coin Toss
2. 背包讲解
(1)01背包:
题目
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
基本思路
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
http://acm.hdu.edu.cn/showproblem.php?pid=2602
#include<iostream>
using namespace std;
int dp[1000][1000];
int max(int x,int y)
{
return x>y?x:y;
}
int main()
{
int t,n,v,i,j;
int va[1000],vo[1000];
cin>>t;
while(t--)
{
cin>>n>>v;
for(i=1;i<=n;i++)
cin>>va[i];
for(i=1;i<=n;i++)
cin>>vo[i];
memset(dp,0,sizeof(dp));//初始化操作
for(i=1;i<=n;i++)
{
for(j=0;j<=v;j++)
{
if(vo[i]<=j)//表示第i个物品将放入大小为j的背包中
dp[i][j]=max(dp[i-1][j],dp[i-1][j-vo[i]]+va[i]);//第i个物品放入后,那么前i-1个物品可能会放入也可能因为剩余空间不够无法放入
else //第i个物品无法放入
dp[i][j]=dp[i-1][j];
}
}
cout<<dp[n][v]<<endl;
}
return 0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=2955
#include<stdio.h>
struct node
{
int money;
double p;
} a[105];
double max(double a,double b)
{ return a>b? a:b;}
int main()
{
int i,j,t,sum,N;
double P;
double dp[10005];
scanf("%d",&t);
while(t--)
{
for(i=0;i<10005;i++)
dp[i]=0;
dp[0]=1;
sum=0;
scanf("%lf%d",&P,&N);
for(i=0;i<N;i++)
{
scanf("%d%lf",&a[i].money,&a[i].p);
sum+=a[i].money;
}
for(i=0;i<N;i++)
for(j=sum;j>=a[i].money;j--)
dp[j]=max(dp[j],dp[j-a[i].money]*(1-a[i].p));
for(i=sum;i>=0;i--)
{
if(dp[i]>1-P)
{
printf("%d\n",i);
break;
}
}
}
return 0;
}
(2)完全背包
题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本思路
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
http://acm.hdu.edu.cn/showproblem.php?pid=2159
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
struct node
{
int val,wei;
} a[155];
int dp[155][155];
int main()
{
intn,m,k,s,x,y,z,i;
while(~scanf("%d%d%d%d",&n,&m,&k,&s))
{
for(i = 1;i<=k; i++)
scanf("%d%d",&a[i].val,&a[i].wei);
memset(dp,0,sizeof(dp));
for(x = 1;x<=m; x++)
{
for(y =1; y<=k; y++)
{
for(z = 1; z<=s; z++)
{
int cnt = 1;
while(cnt*a[y].wei<=x && cnt<=z)
{
dp[x][z] = max(dp[x][z],dp[x-cnt*a[y].wei][z-cnt]+cnt*a[y].val);
cnt++;
}
}
}
if(dp[x][s]>=n)
break;
}
if(x>m)
printf("-1\n");
else
printf("%d\n",m-x);
}
return 0;
}
(3)多重背包
题目
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
基本算法
这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取n[i]件。令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则有状态转移方程:
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}
http://acm.hdu.edu.cn/showproblem.php?pid=2844
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
const int MAX=100000;
int dp[MAX];
int c[MAX],w[MAX];
int v;
void ZeroOnePack(int cost,int wei)//01
{
int i;
for(i = v;i>=cost;i--)
{
dp[i] =max(dp[i],dp[i-cost]+wei);
}
}
void CompletePack(int cost,int wei)//完全
{
int i;
for(i = cost;i<=v;i++)
{
dp[i] =max(dp[i],dp[i-cost]+wei);
}
}
void MultiplePack(int cost,int wei,int cnt)//多重
{
if(v<=cnt*cost)//如果总容量比这个物品的容量要小,那么这个物品可以直到取完,相当于完全背包
{
CompletePack(cost,wei);
return ;
}
else//否则就将多重背包转化为01背包
{
int k = 1;
while(k<=cnt)
{
ZeroOnePack(k*cost,k*wei);
cnt = cnt-k;
k = 2*k;
}
ZeroOnePack(cnt*cost,cnt*wei);
}
}
int main()
{
int n;
while(~scanf("%d%d",&n,&v),n+v)
{
int i;
for(i = 0;i<n;i++)
scanf("%d",&c[i]);
for(i = 0;i<n;i++)
scanf("%d",&w[i]);
memset(dp,0,sizeof(dp));
for(i = 0;i<n;i++)
{
MultiplePack(c[i],c[i],w[i]);
}
int sum = 0;
for(i = 1;i<=v;i++)
{
if(dp[i]==i)
{
sum++;
}
}
printf("%d\n",sum);
}
return 0;
}
3. LIS&&LCS
(1) LIS最长递增子序列
http://acm.hdu.edu.cn/showproblem.php?pid=1003
#include<iostream>
#define N100010
usingnamespace std;
inta[N],d[N];
int main()
{
int test,n,i,max,k,f,e;
cin>>test;
k=1;
while(test--)
{
cin>>n;
for(i=1;i<=n;i++)
cin>>a[i];
d[1]=a[1];
for(i=2;i<=n;i++)
{
if(d[i-1]<0) d[i]=a[i];
else d[i]=d[i-1]+a[i];
}
max=d[1];e=1;
for(i=2;i<=n;i++)
{
if(max<d[i])
{
max=d[i];e=i;
}
}
int t=0;
f=e;
for(i=e;i>0;i--)
{
t=t+a[i];
if(t==max) f=i;
}
cout<<"Case"<<k++<<":"<<endl<<max<<""<<f<<" "<<e<<endl;
if(test) cout<<endl;
}
return 0;
}
(2) LCS最长公共子序列
http://acm.hdu.edu.cn/showproblem.php?pid=1159
#include <iostream>
#include <string.h>
using namespace std;
const int MAX = 1000;
int c[MAX+5][MAX+5];
int lcs(char *x, char *y, int c[][MAX+5])
{
int i, j, m = strlen(x), n =strlen(y);
for(i = 0; i < m; i++)c[i][0] = 0;
for(i = 0; i < n; i++)c[0][i] = 0;
for(i = 0; i < m; i++)
{
for(j = 0; j < n; j++)
{
if(x[i] == y[j])
c[i+1][j+1] =c[i][j] + 1;
else
if(c[i][j+1] >=c[i+1][j])
c[i+1][j+1] =c[i][j+1];
else
c[i+1][j+1] =c[i+1][j];
}
}
return c[m][n];
}
int main()
{
char x[MAX];
char y[MAX];
while(cin >> x >>y)
cout << lcs(x, y, c)<< endl;
return 0;
}
进阶篇
1. 区间dp
区间dp,一般是枚举区间,把区间分成左右两部分,然后求出左右区间再合并。
http://poj.org/problem?id=1141
#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<list>
#include<queue>
#define eps 1e-6
#define INF 0x1f1f1f1f
#define PI acos(-1.0)
#define ll __int64
#define lson l,m,(rt<<1)
#define rson m+1,r,(rt<<1)|1
//#pragma comment(linker,"/STACK:1024000000,1024000000")
using namespace std;
/*
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
*/
#define Maxn 110
char sa[Maxn];
int dp[Maxn][Maxn],path[Maxn][Maxn]; //dp[i][j]表示区间i~j内需要最少的字符数能够匹配,path[i][j]表示到达该状态是哪种情况,
//-1表示第一个和最后一个,其他表示中间的分段点,然后递归输出
//递归能够改变次序
void output(int l,int r) //递归是个好东西
{
if(l>r)
return;
if(l==r)//到达了最后
{
if(sa[l]=='('||sa[l]==')')
printf("()");
else
printf("[]");
return;
}
if(path[l][r]==-1) //首尾,先输出开始,然后递归输出中间,最后输出结尾
{
putchar(sa[l]);
output(l+1,r-1);
putchar(sa[r]);
}
else
{
output(l,path[l][r]);//直接递归输出两部分
output(path[l][r]+1,r);
}
}
int main()
{
while(gets(sa+1)) //有空串,scanf("%s"),不能读空串,然后少一个回车,会出错
{
intn=strlen(sa+1);
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
dp[i][i]=1; //一个的话只需一个就可以匹配
for(int gap=1;gap<n;gap++) //枚举区间长度
for(int i=1;i<=n-gap;i++) //枚举区间开始位置
{
int j=i+gap;
dp[i][j]=INF;
if((sa[i]=='['&&sa[j]==']')||(sa[i]=='('&&sa[j]==')'))//首尾情况
if(dp[i+1][j-1]<dp[i][j])
dp[i][j]=dp[i+1][j-1],path[i][j]=-1;
for(int k=i;k<j;k++) //中间分隔情况
if(dp[i][k]+dp[k+1][j]<dp[i][j])
dp[i][j]=dp[i][k]+dp[k+1][j],path[i][j]=k;
}
output(1,n);
putchar('\n');
}
return 0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=4745
非连续最长回文子序列的长度。
dp[i][j] = max{ dp[i +1][j], d[i][j - 1], (if a[i] == a[j]) dp[i + 1][j - 1] + 2 }
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
#define MAXN 1005
short dp[MAXN][MAXN];
short a[MAXN];
int main()
{
int n;
while(scanf("%d",&n)!=EOF&&n)
{
memset(dp,0,sizeof(dp));
for(int i=0;i<n;i++)scanf("%d",&a[i]);
for(int i=0;i<n;i++) dp[i][i]=1;
for(int i=n-1;i>=0;i--)
for(int j=i+1;j<n;j++)
if(a[i]==a[j])
dp[i][j]=dp[i+1][j-1]+2;
else
dp[i][j]=max(dp[i][j-1],dp[i+1][j]);
intans=dp[0][n-1];
for(int i=0;i<n-1;i++) ans=max(ans,dp[0][i]+dp[i+1][n-1]);
printf("%d\n",ans);
}
return0;
}
2. 树形dp
树形dp是建立在树这种数据结构上的dp,一般状态比较好想,通过dfs维护从根到叶子或从叶子到根的状态转移。
http://poj.org/problem?id=1655
树的重心有下面几条常见性质:
定义1:找到一个点,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心。
定义2:以这个点为根,那么所有的子树(不算整个树自身)的大小都不超过整个树大小的一半。
性质1:树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么他们的距离和一样。
性质2:把两个树通过一条边相连得到一个新的树,那么新的树的重心在连接原来两个树的重心的路径上。
性质3:把一个树添加或删除一个叶子,那么它的重心最多只移动一条边的距离。
#include<stdio.h>
#include<string.h>
#define MAXN 20005
struct Edge{
int v;
int next;
}edge[MAXN*2];
int sum[MAXN*2],son[MAXN];
/*
sum[]记载除去"父节点的子树"的节点数量总和
son[]记录除去"父节点的子树"的所有子树中最大节点数
父节点在该题理解为DFS过程中的上一层节点
*/
int head[MAXN],visited[MAXN];
int tot;
int Max(int a,int b)
{
return a>b ? a :b;
}
void addedge(int x,int y)
{
tot++;
edge[tot].v = y;
edge[tot].next = head[x];
head[x] = tot;
tot++;
edge[tot].v = x;
edge[tot].next = head[y];
head[y] = tot;
}
void dfs(int x)
{
visited[x] = 1;
int t;
for(t=head[x];t!=-1;t=edge[t].next)
{
if(!visited[edge[t].v])
{
dfs(edge[t].v);
sum[x] += sum[edge[t].v];
if(son[x] < sum[edge[t].v])
son[x] = sum[edge[t].v];
}
}
}
int main()
{
int ncase;
scanf("%d",&ncase);
while(ncase--)
{
inti,N;
intx,y;
intid,ans;
scanf("%d",&N);
tot= 0;
memset(head,-1,sizeof(head));
memset(edge,0,sizeof(edge));
for(i=1;i<N;i++)
{
scanf("%d%d",&x,&y);
addedge(x,y);
}
memset(visited,0,sizeof(visited));
memset(son,0,sizeof(son));
for(i=0;i<=N;i++)
sum[i] = 1;
dfs(1);
id= 1;
ans= son[1];
for(i=2;i<N;i++)
if(ans > Max(son[i],sum[1]-sum[i]))//sum[1]为整棵树的节点数量,sum[1]-sum[i]就是父节点子树的节点数目
{
ans = Max(son[i],sum[1]-sum[i]);
id = i;
}
printf("%d %d\n",id,ans);
}
return 0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=4714
#include <vector>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;
vector<int> G[1000005];
int dp[1000005][3],vis[1000005]; //dp[i][j]表示以i为根节点的子树,可用度
void dfs(int s){ //为j时树的分支数,也就是还可以连j个节点
int i,tmp; //时树的分支数
vis[s]=1;
dp[s][0]=dp[s][1]=dp[s][2]=1;
for(i=0;i<G[s].size();i++){
tmp=G[s][i];
if(!vis[tmp]){
dfs(tmp); //两个分支合并则减1
dp[s][0]=min(dp[s][0]+dp[tmp][0],dp[s][1]+dp[tmp][1]-1);
dp[s][1]=min(dp[s][1]+dp[tmp][0],dp[s][2]+dp[tmp][1]-1);
dp[s][2]=dp[s][2]+dp[tmp][0]; //保证s节点为根的子树可用度为2,则不能在向上添加节点
}
}
}
int main(){
inti,j,t,u,v,n,ans;
scanf("%d",&t);
while(t--){
ans=0;
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
scanf("%d",&n);
for(i=1;i<=n;i++)
G[i].clear();
for(i=1;i<n;i++){
scanf("%d%d",&u,&v);
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1);
ans=min(min(dp[1][0],dp[1][1]),dp[1][2]);
printf("%d\n",2*ans-1); //知道分支数,则答案就是分支数-1+分支数,也就是
} //先分离在合并
return 0;
}
3. 数位dp
数位dp,主要用来解决统计满足某类特殊关系或有某些特点的区间内的数的个数,它是按位来进行计数统计的,可以保存子状态,速度较快。
http://acm.hdu.edu.cn/showproblem.php?pid=2089
#include <stdio.h>
#include <string.h>
#include <string.h>
int dp[10][3];//dp[i][j],i代表数字的位数,j代表状况
//dp[i][0],表示不存在不吉利数字
//dp[i][1],表示不存在不吉利数字,且最高位为2
//dp[i][2],表示存在不吉利数字
void Init()
{
memset(dp,0,sizeof(dp));
int i;
dp[0][0]= 1;
for(i =1; i<=6; i++)//数字最长为6
{
dp[i][0] = dp[i-1][0]*9-dp[i-1][1];//最高位加上不含4的9个数字的状况,但因为会放6,所以要减去前一种开头为2的情况
dp[i][1] = dp[i-1][0];//开头只放了2
dp[i][2] = dp[i-1][2]*10+dp[i-1][0]+dp[i-1][1];//已经含有的前面放什么数都可以,或者是放一个4,或者是在2前面放6
}
}
int solve(int n)
{
inti,len = 0,tem = n,ans,flag,a[10];
while(n)//将每一位拆分放入数组
{
a[++len] = n%10;
n/=10;
}
a[len+1]= ans = 0;
flag =0;
for(i=len;i>=1; i--)
{
ans+=dp[i-1][2]*a[i];
if(flag)//如果已经是不吉利了,任意处理
ans+=dp[i-1][0]*a[i];
if(!flag && a[i]>4)//首位大于4,可以有放4的情况
ans+=dp[i-1][0];
if(!flag && a[i+1]==6 && a[i]>2)//后一位为6,此位大于2
ans+=dp[i][1];
if(!flag && a[i]>6)//此位大于6,可能的62状况
ans+=dp[i-1][1];
if(a[i]==4 || (a[i+1]==6&&a[i]==2))//标记为不吉利
flag = 1;
}
returntem-ans;
}
int main()
{
int l,r;
Init();
while(~scanf("%d%d",&l,&r),l+r)
{
printf("%d\n",solve(r+1)-solve(l));
//因为solve函数中并没有考虑n是不是不幸数的情况,所以r+1只算了1~r,而l只算了1~l-1,这两者相减才是正确答案
}
return0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=3709
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
typedef long long LL;
LL dp[20][20][1800];
int digit[20];
LL dfs(int len,int fixloc,int sum,bool fp)
{
if(!len)
return sum == 0 ? 1 : 0;
if(sum < 0)
return 0;
if(!fp&& dp[len][fixloc][sum] != -1)
return dp[len][fixloc][sum];
intfpmax = fp ? digit[len] : 9;
LL ret =0;
for(inti=0;i<=fpmax;i++){
ret+= dfs(len-1,fixloc,sum+i*(len-fixloc),fp && i == fpmax);
}
if(!fp)
dp[len][fixloc][sum] = ret;
returnret;
}
LL f(LL n)
{
if(n ==-1)
return 0;
int len= 0;
while(n){
digit[++len] = n % 10;
n /=10;
}
LL ret =0;
for(inti=len;i>=1;i--){
ret+= dfs(len,i,0,true);
}
returnret - len + 1;
}
int main()
{
memset(dp,-1,sizeof(dp));
int T;
LL a,b;
scanf("%d",&T);
while(T--)
{
scanf("%I64d%I64d",&a,&b);
printf("%I64d\n",f(b)-f(a-1));
}
return0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=4734
#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
int dp[10][200000], mx[10];
int dfs(int len, int pre, bool flag)
{
if (len< 0) return pre >= 0;
if (pre< 0) return 0;
if(!flag && dp[len][pre] != -1) return dp[len][pre];
int end= flag?mx[len]:9, ans = 0;
for (inti = 0; i <= end; ++i) {
ans+= dfs(len-1, pre-i*(1<<len), flag&&i==end);
}
if(!flag) dp[len][pre] = ans;
returnans;
}
int f(int x)
{
int tmp= 1, ans = 0;
while(x) {
ans+= x%10*tmp;
x /=10;
tmp*= 2;
}
returnans;
}
int cal(int a, int b)
{
int top= 0;
while(b) {
mx[top++] = b%10;
b /=10;
}
returndfs(top-1, f(a), true);
}
int main()
{
intiCase = 1, nCase;
int a,b;
scanf("%d", &nCase);
memset(dp, 0xff, sizeof(dp));
while(nCase--) {
scanf("%d %d", &a,&b);
printf("Case #%d: %d\n", iCase++, cal(a, b));
}
return0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=4734
#include <stdio.h>
#include <cstring>
#include <algorithm>
using namespace std;
int dp[10][200000], mx[10];
int dfs(int len, int pre, bool flag)
{
if (len < 0)return pre >= 0;
if (pre < 0)return 0;
if (!flag&& dp[len][pre] != -1) return dp[len][pre];
int end =flag?mx[len]:9, ans = 0;
for (int i = 0;i <= end; ++i) {
ans +=dfs(len-1, pre-i*(1<<len), flag&&i==end);
}
if (!flag)dp[len][pre] = ans;
return ans;
}
int f(int x)
{
int tmp = 1,ans = 0;
while (x) {
ans += x%10*tmp;
x /= 10;
tmp *= 2;
}
return ans;
}
int cal(int a, int b)
{
int top = 0;
while (b) {
mx[top++] =b%10;
b /= 10;
}
returndfs(top-1, f(a), true);
}
int main()
{
int iCase = 1,nCase;
int a, b;
scanf("%d", &nCase);
memset(dp,0xff, sizeof(dp));
while (nCase--){
scanf("%d %d", &a, &b);
printf("Case #%d: %d\n", iCase++, cal(a, b));
}
return 0;
}
4. 概率(期望)dp
一般来说概率正着推,期望逆着推。有环的一般要用到高斯消元解方程。期望可以分解成多个子期望的加权和,权为子期望发生的概率,即 E(aA+bB+...) = aE(A) + bE(B) +...
概率的运算
Ø 两个互斥事件,发生任一个的概率等于两个事件的概率和
Ø 对于不相关的事件或者分步进行的事件,可以使用乘法原则。
Ø 对于一般情况p(A+B)=p(A)+p(B)-p(AB)
http://acm.timus.ru/problem.aspx?space=1&num=1776
#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<algorithm>
#include<vector>
#include<map>
#include<stack>
#include<list>
#include<queue>
#define eps 1e-6
#define INF (1<<30)
#define PI acos(-1.0)
using namespace std;
double dp[450][450];
double sum[450][450];
int main()
{
int n;
while(scanf("%d",&n)!=EOF)
{
n=n-2;
memset(dp,0,sizeof(dp));
memset(sum,0,sizeof(sum));
dp[0][0]=1.0;
for(inti=0;i<=n;i++)
sum[0][i]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++) //假设选择第j个
{
int l=j-1,r=i-j; //左边的个数,右边的个数
int temp=max(l,r)+1;
for(int k=1;k<=temp;k++)
{ //左边恰好用k-1次,右边<=k-1次。或者。。
dp[i][k]+=(dp[l][k-1]*sum[r][k-1]+sum[l][k-1]*dp[r][k-1]
-dp[l][k-1]*dp[r][k-1])/i;
}
}
for(int j=1;j<=n;j++)
sum[i][j]=sum[i][j-1]+dp[i][j];
}
doubleans=0;
for(int i=1;i<=n;i++)
ans+=dp[n][i]*i;
printf("%.10lf\n",ans*10.0);
}
return 0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=4418
.
期望:E[x] = sum((E[x+i]+i) * p[i])(i∈[1, m])
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <queue>
#include <algorithm>
#include <math.h>
using namespace std;
#define M 205
#define eps 1e-8
int equ, var;
double a[M][M], x[M];
int Gauss ()
{
int i, j,k, col, max_r;
for (k =0, col = 0; k < equ && col < var; k++, col++)
{
max_r= k;
for(i = k+1; i < equ; i++)
if(fabs (a[i][col]) > fabs (a[max_r][col]))
max_r= i;
if(k != max_r)
{
for(j = col; j < var; j++)
swap(a[k][j], a[max_r][j]);
swap(x[k], x[max_r]);
}
x[k]/= a[k][col];
for(j = col+1; j < var; j++) a[k][j] /= a[k][col];
a[k][col]= 1;
for(i = 0; i < equ; i++) if (i != k)
{
x[i]-= x[k] * a[i][k];
for(j = col+1; j < var; j++) a[i][j] -= a[k][j] * a[i][col];
a[i][col]= 0;
}
}
return 1;
}
//has[x]表示人在x点时的变量号,因为我们只用可达状态建立方程,所以需要编号
int has[M], vis[M], k, e, n, m;
double p[M], sum;
int bfs (int u)
{
memset(has, -1, sizeof(has));
memset(a, 0, sizeof(a)); //忘记初始化WA勒,以后得注意
memset(vis, 0, sizeof(vis));
int v, i,flg = 0;
queue<int>q;
q.push(u);
k = 0;
has[u] =k++;
while(!q.empty ())
{
u =q.front ();
q.pop();
if(vis[u]) continue;
vis[u]= 1;
if(u == e || u == n-e) //终点有两个,你懂的~
{
a[has[u]][has[u]]= 1;
x[has[u]]= 0;
flg= 1;
continue;
}
//E[x]= sum ((E[x+i]+i) * p[i])
//----> E[x] - sum(p[i]*E[x+i]) = sum(i*p[i])
a[has[u]][has[u]]= 1;
x[has[u]]= sum;
for(i = 1; i <= m; i++)
{
//非常重要!概率为0,该状态可能无法到达,如果还去访问并建立方程会导致无解
if(fabs (p[i]) < eps) continue;
v= (u + i) % n;
if(has[v] == -1) has[v] = k++;
a[has[u]][has[v]]-= p[i];
q.push(v);
}
}
returnflg;
}
int main()
{
int t, s,d, i;
scanf("%d", &t);
while(t--)
{
scanf("%d%d%d%d%d", &n, &m, &e, &s, &d);
n =2*(n-1);
sum= 0;
for(i = 1; i <= m; i++)
{
scanf("%lf", p+i);
p[i]= p[i] / 100;
sum+= p[i] * i;
}
if(s == e)
{
puts("0.00");
continue;
}
//一开始向左,起点要变
if(d > 0) s = (n - s) % n;
if(!bfs (s))
{
puts("Impossible !");
continue;
}
equ= var = k;
Gauss();
printf("%.2f\n",x[has[s]]);
}
return0;
}
http://acm.hdu.edu.cn/showproblem.php?pid=4586
#include<iostream>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<stack>
#include<list>
#include<queue>
#define eps 1e-6
#define INF 0x1f1f1f1f
#define PI acos(-1.0)
#define ll __int64
#define lson l,m,(rt<<1)
#define rson m+1,r,(rt<<1)|1
#pragma comment(linker,"/STACK:1024000000,1024000000")
using namespace std;
//freopen("data.in","r",stdin);
//freopen("data.out","w",stdout);
int main()
{
int n,m;
while(~scanf("%d",&n))
{
intsum=0,a;
for(int i=1;i<=n;i++)
scanf("%d",&a),sum+=a;
scanf("%d",&m);
for(int i=1;i<=m;i++)
scanf("%d",&a);
if(sum==0)
printf("0.00\n");
else if(n==m)
printf("inf\n");
else
printf("%.2f\n",sum*1.0/(n-m));
}
return 0;
}
5. 状态压缩dp
我们知道,用DP解决一个问题的时候很重要的一环就是状态的表示,一般来说,一个数组即可保存状态。但是有这样的一些题目,它们具有DP问题的特性,但是状态中所包含的信息过多,如果要用数组来保存状态的话需要四维以上的数组。于是,我们就需要通过状态压缩来保存状态,而使用状态压缩来保存状态的DP就叫做状态压缩DP。
http://acm.hdu.edu.cn/showproblem.php?pid=4529
定义状态dp[i][j][p][q]:表示前i行,使用了j个骑士,第i行的状态为p,第i-1行的状态为q的方案数。
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int t,N;
char s[10][10];
bool suit[10][1<<8+5];
intdp[8][11][1<<8][1<<8];//dp[i][j][a][b],第i行,前i行j个骑士,i行状态a,i-1行状态b的方案数
int one[1<<8+5];
//处理出每行合法的状态
inline void init()
{
memset(suit,0,sizeof(suit));
for(inti=0;i<8;i++)
{
for(int j=0;j< 1<<8;j++)
{
int tag=1;
for(int k=0;k<8;k++)
{
if(s[i][k]=='*'&&(j&(1<<k)))
{
tag=0; break;
}
}
if(tag)suit[i][j]=1;
}
}
}
inline int getOne(int i)
{
intans=0;
while(i)
{
ans+=i%2;
i/=2;
}
returnans;
}
int main()
{
for(inti=0;i<(1<<8);i++)
one[i]=getOne(i);
scanf("%d",&t);
while(t--)
{
scanf("%d",&N);
for(int i=0;i<8;i++) scanf("%s",s[i]);
init();
memset(dp,0,sizeof(dp));
for(int i=0;i<(1<<8);i++)
{
if(suit[0][i] && one[i]<=N)
{
dp[0][one[i]][i][0]=1;
}
}
for(int i=1;i<8;i++)
for(int n=0;n<=N;n++)
for(int j=0;j<(1<<8);j++)
{
if(one[j]> n) continue;
if(!suit[i][j])continue;
for(intk=0;k< 1<<8;k++)
{
if(k& (j<<2)) continue;
if(k& (j>>2)) continue;
for(intr = 0;r < (1<<8);r++)
{
if(r& (j<<1)) continue;
if(r& (j>>1)) continue;
dp[i][n][j][k]+=dp[i-1][n-one[j]][k][r];
}
}
}
intans=0;
for(int i=0;i<(1<<8);i++)
{
if(suit[7][i])
{
for(int j=0;j<(1<<8);j++)
{
if(suit[6][j])
ans+=dp[7][N][i][j];
}
}
}
printf("%d\n",ans);
}
}
http://poj.org/problem?id=1185
思考方法:首先,一个炮的攻击有两行,所以对于第i行来讲,i-1行和i-2行对它有影响,i-3行及以上的都没有影响了,所以我们要得到第i行的信息,只需要知道i-1和i-2的信息(最近有个体会,DP要找到什么因素影响了当前你要求的东西,有影响的我们就处理,没影响的我们不用管)。接着我们就思考怎么表示状态。山用1表示,空地用0表示,空地放了兵也用1表示,那么对于一行,就是一个01的串,这是个二进制数,我们可以想到状态压缩压缩回来一个十进制数。
比如原地图01101011,那么0处可以放兵,所有那么多个0,可以变为1(但也要考虑炮与炮之间不能攻击),要枚举全部情况,我们很自然想到了dfs来枚举,很多解题报告是这样做的,这样确实也能解决,但不是最好的方法。最好的方法是位运算。
要想到位运算,要跳出思维的限制,在一行中,原有的1是固定不能变,炮不能放在山上。0是可以变为1的,但是要保证炮与炮之间不能攻击。要满足这两个要求,我们可以拆开来做。先满足了炮与炮不能互相攻击,然后在这些摆放中再选出跑不在山上的。
只考虑炮的话,枚举量2^10-1,但实际上满足的不足60个,网上有人问过60是怎么计算的,实际上准确的做法我也不确定,但是能大概推出来。一列最多10位,最多其实只能放4个炮,然后接着看3个炮,2个炮,1个炮,0个炮的情况,就可以大致算出。
枚举方法是 for(i=0; i<(i<<col);i++) if( !(i&(i<<1)) & !(i&(i<<2)) ) i是合法状态 这个是要点,要理解
intstate[MAXM]; 保存状态(十进制数),是仅仅满足了炮与炮不互相攻击,但是没有满足炮不在山上
对于一开始的地图,还没放炮,它本身已经表示了一个状态,所以也先压成一个状态,保存在一个数组中
再者,我们得到了一个状态state[i],我们怎么知道这个状态下放了多少炮啊?其实就是判断state[i]这个十进制数变为二进制数后有多少个1,这个要怎么统计呢?位运算!
并且把对应的士兵人数保存在 int soldier[MAXM]; //对应着,在state[i]状态下能放多少个士兵
intbase[MAXR]; //第i行的原地图压缩成的一个状态
那么怎么判断炮不在山上呢? 只要state[i]& base[r] = 0 ,就表示state[i]这个状态,可以放在r这行上,而且炮不会在山上,炮之间也不会攻击,这是个要点,理解
然后前面说了,i行,i-1行,i-2行的炮会互相影响,他们可能会互相攻击到对方,所以我们假设现在i行,i-1行,i-2行的炮的摆放情况分别是state[i],state[j],state[k]
只有当他们都不两两攻击的时候,这3个状态才能放在一起,否则这3个状态不能放在一起。那么怎么判断他们不会两两攻击呢,方法一样的
state[i] &state[j] = 0 state[i] & state[k] = 0 state[j] &state[k] = 0 ,三个要同时满足,要点,理解
你会发现多次需要用到 一种判断 a&b 是为0还是不为0,所以我们代码中将其写成宏定义方便查看,实际上写成宏后时间慢了一点
#definelegal(a,b) (a&b) //判断两个状态共存时是否合法,合法为0,不合法为非0
最后看状态转移方程,设dp[r][i]表示第r行,状态为state[i]是的最大值,
dp[r][i]=max {dp[r-1][j]+dp[r-2][k] } + soldier[i]
也就是第r-1行的状态为state[j],第r-2行的状态为state[k]的和,并加上当前行放了soldier[i]个士兵 , 但要满足state[i],state[j],state[k]不能互相攻击
接着我们可以可以写成三维的形式 dp[r][i][j]= max{ dp[r-1][j][k]} +soldier[i] , dp[r][i][j]表示第r行状态为state[i],第r-1行为state[j]的最大值
#include <cstdio>
#include <cstring>
#define MAXR 110 //行数
#define MAXC 15 //列数
#define MAXM 70 //状态数
#define max(a,b) a>b?a:b //返回较大值
#define CL(a) memset(a,0,sizeof(a)) //初始化清空数组
#define legal(a,b) a&b //判断两个状态共存时是否合法,合法为0,不合法为非0
int row,col; //行列
int nums; //仅是两个炮兵不互相攻击的条件下,符合条件的状态个数
int base[MAXR]; //第i行的原地图压缩成的一个状态
int state[MAXM]; //仅是两个炮兵不互相攻击的条件下,符合条件的状态(一个十进制数)
int soldier[MAXM]; //对应着,在state[i]状态下能放多少个士兵
int dp[MAXR][MAXM][MAXM];
//dp[i][j][k] 表示第i行状态为state[j],第i-1行状态为state[k]时的最优解
char g[MAXR][MAXC];
int main()
{
CL(base); CL(state); CL(soldier); CL(dp);
nums=0;
scanf("%d%d",&row,&col);
for(inti=0; i<row; i++) //先计算原始地图的状态数
{
scanf("%s",g[i]);
for(int j=0; j<col; j++)
if(g[i][j]=='H') base[i]+=1<<j; //像0110000,这里计算为6
}
for(inti=0; i<(1<<col); i++) //仅是两个炮兵不互相攻击的条件下计算所有状态
{
if(legal(i,i<<1) || legal(i,i<<2)) continue; //i这个状态出现了士兵两两攻击
intk=i;
while(k) //这个循环计算状态i的二进制形式里面有多少个1,也就是放了多少个士兵
{
soldier[nums]+=k&1; //等价于k%2,相当于判断k的二进制形式里面有多少个1
k=k>>1;
}
state[nums++]=i; //保存这个合法的状态
}
/***************************************/
//for(int i=0; i<nums; i++) printf("%d%d\n",state[i],soldier[i]);
/***************************************/
for(inti=0; i<nums; i++) //先初始化dp[0][i][0],即初始化第1行的情况
{
if(legal(state[i],base[0])) continue;
//在state[i]的基础上,还要满足士兵不能放在山上,这个判断就是处理这个问题的
dp[0][i][0]=soldier[i];
}
for(inti=0; i<nums; i++) //接着初始化dp[1][i][j],即第2行的情况
{
if(legal(state[i],base[1])) continue;
for(int j=0; j<nums; j++) //枚举第1行的状态
{
if(legal(state[j],base[0])) continue;
if(legal(state[i],state[j])) continue;
dp[1][i][j]=max(dp[1][i][j] , dp[0][j][0]+soldier[i]);
//状态转移方程
}
}
for(intr=2; r<row; r++) //第3行开始DP直到最后
for(int i=0; i<nums; i++) //枚举第r行的状态
{
if(legal(state[i],base[r])) continue;
for(int j=0; j<nums; j++) //枚举第r-1行的状态
{
if(legal(state[j],base[r-1])) continue;
if(legal(state[i],state[j])) continue;
//第r行的士兵和第r-1行的士兵相互攻击
for(int k=0; k<nums; k++) //枚举第r-2行的状态
{
if(legal(state[k],base[r-2])) continue;
if(legal(state[j],state[k])) continue;
//第r-1行的士兵和第r-2行的士兵相互攻击
if(legal(state[i],state[k])) continue;
//第r行的士兵和第r-1
dp[r][i][j]=max(dp[r][i][j] , dp[r-1][j][k]+soldier[i]);
}
}
}
intans=0;
for(inti=0; i<nums; i++)
for(int j=0; j<nums; j++) //枚举dp[row-1][i][j]
ans=max(ans,dp[row-1][i][j]);
printf("%d\n",ans);
return0;
}
6. 数据结构优化的dp
(1) 二进制优化
http://acm.split.hdu.edu.cn/showproblem.php?pid=1059
方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,...,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。
分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分0..2^k-1和2^k..n[i]两段来分别讨论得出
#include<stdio.h>
#include<string.h>
#define max(a,b) a>b?a:b
int Va[100],We[100],n[7],dp[200000];
int main(){
int c=0;
while(scanf("%d%d%d%d%d%d",&n[1],&n[2],&n[3],&n[4],&n[5],&n[6])!=EOF&&(n[1]!=0||n[2]!=0||n[3]!=0||n[4]!=0||n[5]!=0||n[6]!=0)){
printf("Collection#%d:\n",++c);
inti,j,sum=0,mid;
for(i=1;i<=6;i++)sum+=i*n[i];
if(sum%2){
printf("Can't be divided.\n\n");
continue;
}
elsemid=sum/2;
memset(dp,-1,sizeof(dp));
intcount=0,temp;
for(i=1;i<=6;i++){
temp=1;
while(n[i]>=temp){
Va[++count]=i*temp;
We[count]=temp;
n[i]-=temp;
temp*=2;
}
if(n[i]>0){
Va[++count]=i*n[i];
We[count]=n[i];
}
}
dp[0]=0;
for(i=1;i<=count;i++){
for(j=mid;j>=Va[i];j--){
if(dp[j-Va[i]]>=0){
dp[j]=max(dp[j],dp[j-Va[i]]+We[i]);
}
}
}
if(dp[mid]>0)printf("Can be divided.\n\n");
elseprintf("Can't be divided.\n\n");
}
return0;
}
(2) 单调队列优化
http://acm.hdu.edu.cn/showproblem.php?pid=3401
.解题思路:
dp[i][j]表示前i天当持有j股股票时,获得的最大利益。
状态转移:
当第i天不交易时为dp[i-1][j];
当第i天买(j-k)股时为dp[i-w-1][k]-(j-k)*b[i] 0=<k<=j //注意要求第i-w----i-1天都不能交易.
卖(k-j)股时为dp[i-w-1][k]+(k-j)*s[i] j=<k<=p
当固定某个j时,dp[i-w-1][k]+k*b[i]是固定的(k,j有个关系),所以可以用单调队列优化。
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cstring>
#include<algorithm>
#include<vector>
#include<map>
#include<stack>
#include<list>
#include<queue>
#define eps 1e-6
#define INF 0x1f1f1f1f
#define PI acos(-1.0)
#define ll __int64
#define lson l,m,(rt<<1)
#define rson m+1,r,(rt<<1)|1
using namespace std;
/*
freopen("data.in","r",stdin);
freopen("data.out","w",stdout);
*/
intdp[2200][2200],b[2200],s[2200],bl[2200],sl[2200];
struct Node
{
intnu,mo;
}myd[2200];
int main()
{
intw,t,p,ca;
scanf("%d",&ca);
while(ca--)
{
scanf("%d%d%d",&t,&p,&w);
for(int i=1;i<=t;i++)
scanf("%d%d%d%d",&b[i],&s[i],&bl[i],&sl[i]);
memset(dp,-INF,sizeof(dp));
for(int j=1;j<=t;j++) //所有股票都是当天买的
for(int i=0;i<=min(bl[j],p);i++)
dp[j][i]=max(dp[j][i],-i*b[j]);
for(int i=2;i<=t;i++)
{
dp[i][0]=0;
for(int j=0;j<=p;j++) //没有交易
dp[i][j]=max(dp[i-1][j],dp[i][j]);
if(i<w+2) //不能交易
continue;
intpre=i-w-1;
intss=0,e=-1;
for(int j=0;j<=p;j++) //买
{
int tmp=dp[pre][j]+j*b[i];
while(ss<=e&&tmp>=myd[e].mo)
e--;
struct Node te;
te.nu=j,te.mo=tmp;
myd[++e]=te;
while(ss<=e&&j-myd[ss].nu>bl[i])
ss++;
dp[i][j]=max(dp[i][j],myd[ss].mo-j*b[i]);
}
ss=0,e=-1;
//memset(myd,0,sizeof(myd));
for(int j=p;j>=0;j--) //卖
{
int tmp=dp[pre][j]+j*s[i];
while(ss<=e&&tmp>=myd[e].mo)
e--;
struct Node te;
te.nu=j;te.mo=tmp;
myd[++e]=te;
while(ss<=e&&myd[ss].nu-j>sl[i])
ss++;
dp[i][j]=max(dp[i][j],myd[ss].mo-j*s[i]);
}
}
intans=0;
for(int i=0;i<=p;i++)
ans=max(ans,dp[t][i]);
printf("%d\n",ans);
}
return 0;
}
(3) 斜率优化
http://acm.hdu.edu.cn/showproblem.php?pid=2993
给定一个长度为n的序列,从其中找连续的长度大于m的子序列使得子序列中的平均值最小。
详细分析见:
NOI2004年周源的论文《浅谈数形结合思想在信息学竞赛中的应用》,
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
const int N = 100001;
double sum[N];
int a[N];
int n, len;
int q[N], head, tail;
double max(double a,double b)
{
return a< b ? b : a;
}
int main()
{
while(scanf("%d %d", &n, &len) == 2)
{
sum[0] = 0;
for(int i = 1;i <= n;i++)
{
scanf("%d", &a[i]);
sum[i] = sum[i - 1] + a[i];
}
head= tail = 0;
double ans = 0;
for(int i = len;i <= n;i++)
{
int now = i - len;
while(head + 1 < tail)
{
double dy1 = sum[q[tail - 1]] - sum[q[tail - 2]], dy2 = sum[now] -sum[q[tail - 1]];
double dx1 = q[tail - 1] - q[tail - 2],dx2 = now - q[tail - 1];
if(dy1 * dx2 > dy2 * dx1)
tail--;
else
break;
}
q[tail++] = now++;
while(head + 1 < tail)
{
double y1 = sum[q[head]], y2 = sum[q[head + 1]],y3 = sum[i];
double x1 = q[head], x2 = q[head + 1], x3 = i;
if((y3 - y1) * (x3 - x2) < (y3 - y2) * (x3 - x1))
head++;
else
break;
}
ans = max(ans,(sum[i] - sum[q[head]]) / (i - q[head]));
}
printf("%.2lf\n", ans);
}
return0;
现在来分析解题思路:
首先一定会设序列ai的部分和:Si=a1+a2+…+ai,,特别的定义S0=0。
这样可以很简洁的表示出目标函数ave(i,j)=(Sj-S(i-1))/(j-(i-1))!
如果将S函数绘在平面直角坐标系内,这就是过点Sj和点Si-1直线的斜率!
于是问题转化为:平面上已知N+1 个点,Pi(i,Si),0≤i≤N,求横向距离大
于等于F的任意两点连线的最大斜率。
构造下凸折线
有序化一下,规定对i<j,只检查Pj向Pi的连线,对Pi不检查与Pj的连线。
也就是说对任意一点,仅检查该点与在其前方的点的斜率。于是我们定义点Pi
的检查集合为
Gi = {Pj,0≤j≤i-F}
特别的,当i<F时,Gi为空集。
其明确的物理意义为:在平方级算法中,若要检查ave(a, b),那么一定有Pa∈Gb;
因此平方级的算法也可以这样描述,首先依次枚举Pb点,再枚举Pa∈Gb,同时检查k(PaPb)。//k为斜率
若将Pi和Gi同时列出,则不妨称Pi为检查点,Gi中的元素都是Pi的被检查点。
当我们考察一个点Pt时,朴素的平方级算法依次选取Gt中的每一个被检查点p,
考察直线pPt的斜率。但仔细观察,若集合内存在三个点Pi, Pj, Pk,且i<j<k,三个点形成如下图
所示的的关系,即Pj点在直线PiPk的上凸部分:k(Pi, Pj)>k(Pj,Pk),就很容易可以证明Pj点是多余的。
若k(Pt, Pj) > k(Pt, Pi),那么可以看出,Pt点一定要在直线PiPj的上方,即阴
影所示的1号区域。同理若k(Pt, Pj) > k(Pt, Pk),那么Pt点一定要在直线PjPk的下
方,即阴影所示的2号区域。
综合上述两种情况,若PtPj的斜率同时大于PtPi和PtPk的,Pt点一定要落在两阴影的重叠部分,
但这部分显然不满足开始时t>j 的假设。于是,Pt落在任何一个合法的位置时,PtPj的斜率要么小于PtPi,
要么小于PtPk,即不可能成为最大值,因此Pj点多余,完全可以从检查集合中删去。这个结论告诉我们,
任何一个点Pt的检查集合中,不可能存在一个对最优结果有贡献的上凸点,因此我们可以删去每一个上凸点,
剩下的则是一个下凸折线。最后需要在这个下凸折线上找一点与Pt 点构成的直线斜率最大——显然这条直
线是在与折线相切时斜率最大,如图所示。
这一小节中,我们的目标是:用尽可能少的时间得到每一个检查点的下凸折线。
算法首先从PF开始执行:它是检查集合非空的最左边的一个点,集合内仅有一个元素P0,
而这显然满足下凸折线的要求,接着向右不停的检查新的点:PF+1,PF+2, …, PN。
检查的过程中,维护这个下凸折线:每检查一个新的点Pt,就可以向折线最右端加入一个新的点Pt-F,
同时新点的加入可能会导致折线右端的一些点变成上凸点,我们用一个类似于构造凸包的过程依次删去这些上凸点,从而保证折线的下凸性。由于每个点仅被加入和删除一次,所以每次维护下凸折线的平摊复杂度为O(1),
即我们用O(N)的时间得到了每个检查集合的下凸折线。
最后的优化:利用图形的单调性
最后一个问题就是如何求过Pt点,且与折线相切的直线了。一种直接的方法就是二分,每次查找的复杂度是O(log2N)。但是从图形的性质上很容易得到另一种更简便更迅速的方法:
由于折线上过每一个点切线的斜率都是一定的,而且根据下凸函数斜率的单调性,如果在检查点Pt 时找到了折线上的已知一个切点A,那么A以前的所有点都可以删除了:过这些点的切线斜率一定小于已知最优解,不会做出更大的贡献了。
于是另外保留一个指针不回溯的向后移动以寻找切线斜率即可,平摊复杂度为为O(1)。
至此,此题算法时空复杂度均为O(N),得到了圆满的解决。
(4) 四边形不等式优化
当函数w(i,j)满足 w(a,c)+w(b,d) <= w(b,c)+w(a,d) 且a<=b< c <=d 时,我们称w(i,j)满足四边形不等式。。
当函数w(i, j)满足w(i',j) <= w(i, j'); i <= i' < j <= j' 时,称w关于关于区间包含关系单
调。
s(i, j)=k是指m(i, j)这个状态的最优决策
以上定理的证明自己去查些资料
今天看得lrj的书中介绍的 四边形优化 做个笔记,加强理解
最有代价用d[i,j]表示
d[i,j]=min{d[i,k-1]+d[k+1,j]}+w[i,j]
其中w[i,j]=sum[i,j]
四边形不等式
w[a,c]+w[b,d]<=w[b,c]+w[a,d](a<b<c<d)就称其满足凸四边形不等式
决策单调性
w[i,j]<=w[i',j'] ([i,j]属于[i',j']) 既i'<=i<j<=j'
于是有以下三个定理
定理一: 如果w同时满足四边形不等式 和 决策单调性 ,则d也满足四边形不等式
定理二:当定理一的条件满足时,让d[i,j]取最小值的k为K[i,j],则K[i,j-1]<=K[i,j]<=K[i+1,j]
定理三:w为凸当且仅当w[i,j]+w[i+1,j+1]<=w[i+1,j]+w[i,j+1]
由定理三知 判断w是否为凸即判断 w[i,j+1]-w[i,j]的值随着i的增加是否递减
于是求K值的时候K[i,j]只和K[i+1,j] 和K[i,j-1]有关,所以 可以以i-j递增为顺序递推各个状态值最终求得结果 将O(n^3)转为O(n^2)
http://acm.hdu.edu.cn/showproblem.php?pid=3516
这个题看起来很像石子合并最小得分,但是却和石子合并不同,石子合并的w[i][j]值是恒定的,而这个题的w[i][j]却随决策变量k的不同而不同。所以这个题不能用贪心,需要用dp。
dp方程:f[i][j] = f[i][k]+f[k+1][j] + w[i][j]
但是这里有个问题,w[i][j]的取值和决策k是有关系的,不能直接看出是否满足四边形不等式。
可以这么想:如果我们固定k,由于w[i][j]= p[k+1].x-p[i].x+p[k].y-p[j].y,所以固定j时,w[i][j+1]-w[i][j]= (p[k+1].x-p[i].x+p[k].y-p[j].y) - (p[k+1].x-p[i].x+p[k].y-p[j+1].y) =(-p[j].y)-(-p[j+1].y) = p[j+1].y - p[j].y < 0 == c(因为题意说明:p[j+1].y严格小于p[j].y)。所以对于所有决策k,w[i][j+1]-w[i][j]随i增加始终是个常数,当然也可以说成是非递增或这说是不严格递减,所以可以认为w[i][j]是凸的;区间包含关系显而易见。所以综上,w[i][j]对于所有决策k均满足四边形不等式,所以f[i][j]也满足四边形不等式,所以可以到处决策单调性:s[i][j-1]<=s[i][j]<=s[i+1][j]。
#include <iostream>
using namespace std;
#define M 1005
#define INF 1000000000
int n,f[M][M],sum[M][M],s[M][M];
struct POINTS
{
int x,y;
}stone[M];
int dis(POINTS a,POINTS b,POINTS c,POINTSd)//calculate from a to d; b,c is division point
{
returnabs(c.x-a.x)+abs(d.y-b.y);
}
int main()
{
inti,j,k,t;
while(cin>>n)
{
for(i=1;i<=n;i++)
scanf("%d%d",&stone[i].x,&stone[i].y);
memset(f,0,sizeof(f));
for(i=1;i<=n;i++)
{
s[i][i]=i;
}
for(intlen=2;len<=n;len++)//归并的石子长度
{
for(i=1;i<=n-len+1;i++)//i为起点,j为终点
{
j=i+len-1;
f[i][j]=INF;
for(k=s[i][j-1];k<=s[i+1][j];k++)
{
if(f[i][j]>f[i][k]+f[k+1][j]+dis(stone[i],stone[k],stone[k+1],stone[j]))
{
f[i][j]=f[i][k]+f[k+1][j]+dis(stone[i],stone[k],stone[k+1],stone[j]);
s[i][j]=k;
}
}
}
}
printf("%d/n",f[1][n]);
}
return 0;
}
END
DP总结 强烈推荐:http://doc.okbase.net/cc_again/archive/71796.html
推荐博客及论文:
1.经典的背包九讲:http://love-oriented.com/pack/
2.推荐论文:
3.推荐论文:http://wenku.baidu.com/view/ce445e4f767f5acfa1c7cd51.html
4.推荐论文:http://wenku.baidu.com/view/4d23b4d128ea81c758f578ae.html
5.推荐论文:用单调性优化动态规划