dp
CODEVS1006:
给定n(1<=n<=100)个数,从中找出尽可能多的数使得他们能够组成一个等差数列.求最长的等差数列的长度.
思路:穷举,n^3的时间复杂度,稳过。
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int a[101]={0}; int main() { int sum,ans=0,n,i,j,k,ch,la; cin>>n; if (n<=2) cout<<n<<endl; else { for (i=1;i<=n;++i) cin>>a[i]; sort(a+1,a+n+1); for (i=1;i<n;++i) for (j=i+1;j<=n;++j) { ch=a[j]-a[i]; la=j; sum=2; for (k=j+1;k<=n;++k) if (a[k]-a[la]==ch) { la=k; ++sum; } if (sum>ans) ans=sum; } cout<<ans<<endl; } }
CODEVS2081:
一个等差数列是一个能表示成a, a+b, a+2b,..., a+nb (n=0,1,2,3,...)的数列。
在这个问题中a是一个非负的整数,b是正整数。写一个程序来找出在双平方数集合(双平方数集合是所有能表示成p的平方 + q的平方的数的集合,其中p和q为非负整数)S中长度为n的等差数列。
思路:先算出双平方数集合,m^2的食府,去重。然后,穷举起点和差(即等差数列的第二项),一开始有穷举了后面所有的项,n^3算法,超时稳稳地;后来做了个bool数组,然后直接加差值,若不存在就break,若当前值大于最大值也break,n^2m的算法,稳过。
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; struct use{ int a,b; }ans[100001],an; int shu[200000]={0}; bool us[200000]={false}; int my_comp(const use &x,const use &y) { if (x.b<y.b) return 1; if (x.b==y.b&&x.a<y.a) return 1; return 0; } int main() { int sum,n,m,i,j,k,tot=0,size,ansi=0,la,maxn=0; scanf("%d%d",&n,&m); for (i=0;i<=m;++i) for (j=i;j<=m;++j) { ++tot; shu[tot]=i*i+j*j; us[shu[tot]]=true; maxn=max(maxn,shu[tot]); } sort(shu+1,shu+tot+1); size=unique(shu+1,shu+tot+1)-shu-1; tot=size; ansi=1; for (i=1;i<tot;++i) for (j=i+1;j<=tot;++j) { an.a=shu[i]; an.b=shu[j]-shu[i]; la=shu[j]; sum=2; while (sum<n) { if (la>=maxn) break; if (!us[la+an.b]) break; la=la+an.b; ++sum; } if (sum==n) { ans[ansi].a=an.a; ans[ansi].b=an.b; ++ansi; } } --ansi; if (ansi==0) printf("NONE\n"); else { sort(ans+1,ans+ansi+1,my_comp); for (i=1;i<=ansi;++i) printf("%d %d\n",ans[i].a,ans[i].b); } }
CODEVS2205:
等差数列的定义是一个数列S,它满足了(S[i]-S[i-1]) = d (i>1)。显然的一个单独的数字或者两个数字也可以形成一个等差数列。
经过一定的学习小C发现这个问题太简单了,等差数列的和不就是(Sn+S1)*n/2?因为这个问题实在是太简单了,小C不屑于去解决它。这让小C的老师愤怒了,他就找了另外一个问题来问他。
小C的老师给了他一个长度为N的数字序列,每个位置有一个整数,他需要小C帮他找到这个数字序列里面有多少个等差数列。
这个问题似乎太难了,小C需要你的程序帮他来解决这个问题。
思路:f[i][j]表示以i为终点(且i不为起点),差为j的个数,穷举起点和第二项,然后更新第二项的值。最后就需要加上n,以为一个元素也是等差数列。
#include<iostream> #include<cstdio> using namespace std; long long f[1001][5000]={0},a[1001]={0}; int main() { int n,i,j,cha; long long ans=0; scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d",&a[i]); for (i=1;i<n;++i) for (j=i+1;j<=n;++j) { cha=a[j]-a[i]+1000; f[j][cha]=(f[i][cha]+f[j][cha]+1)%9901; } ans=n; for (i=1;i<=n;++i) for (j=0;j<=2000;++j) ans=(ans+f[i][j])%9901; printf("%lld\n",ans); }
邮票面值设计
问题描述
给定一个信封,最多只允许粘贴N张邮票,计算在给定K(N+K≤40)种邮票的情况下(假定所有的邮票数量都足够),如何设计邮票的面值,能得到最大值MAX,使在1~MAX之间的每一个邮资值都能得到。
思路:深搜加上dp。每次深搜中j的取值范围从上一次+1到上一次能取到的最大值+1,能减少循环次数。对于最大值的取得情况用dp,类似于背包。
#include<iostream> #include<cstring> using namespace std; int n,k,a[41]={0},ansd[41]={0},ans=0,piece[30001]={0}; int rmb(int i) { int j,t,maxx; memset(piece,0,sizeof(piece)); for (j=1;j<=i;++j) piece[a[j]]=1; maxx=1; do { ++maxx; for (j=1;j<=i;++j) if (maxx-a[j]>=0) { if (piece[maxx]==0) piece[maxx]=piece[maxx-a[j]]+1; if (piece[maxx]>piece[maxx-a[j]]+1) piece[maxx]=piece[maxx-a[j]]+1; } if ((piece[maxx]==0)||(piece[maxx]>n)) return maxx-1; } while (true); } void dfs(int i,int maxn) { int j; if (i==k+1) { if (maxn>ans) { ans=maxn; for (j=2;j<=k;++j) ansd[j]=a[j]; } return; } for (j=a[i-1]+1;j<=maxn+1;++j) { a[i]=j; maxn=rmb(i); dfs(i+1,maxn); } } int main() { int i; cin>>n>>k; a[1]=1; ansd[1]=1; dfs(2,n); for (i=1;i<=k;++i) cout<<ansd[i]<<" "; cout<<endl<<"MAX="<<ans<<endl; }
CODEVS1253 超级市场
某人喜欢按照自己的规则去市场买菜,他每天都列一个买菜的清单,自由市场的菜码放也有一个顺序,该人有一个特点,就是按顺序买菜,从不走回头路,当然,她希望能花最好的钱买到所有的菜,你能帮帮他吗?
输入输出数据如下图:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; struct use{ int kind; double mon; }b[100001]; double f[100001][101]={0}; int a[101]={0}; int main() { int i,j,n,m,k; scanf("%d%d",&m,&n); memset(f,127,sizeof(f)); for (i=1;i<=m;++i) scanf("%d",&a[i]); for (i=1;i<=n;++i) scanf("%d%lf",&b[i].kind,&b[i].mon); for(i=1;i<=n;++i) f[i][0]=0; for (i=1;i<=n;++i) for (j=1;j<=m;++j) { f[i][j]=f[i-1][j]; if (b[i].kind==a[j]) f[i][j]=min(f[i][j],f[i-1][j-1]+b[i].mon); } if (f[n][m]>0x7fffffff) printf("Impossible\n"); else printf("%.2f\n",f[n][m]); }
CODEVS1959 拔河游戏
一个学校举行拔河比赛,所有的人被分成了两组,每个人必须(且只能够)在其中的一组,要求两个组的人数相差不能超过1,且两个组内的所有人体重加起来尽可能地接近。
思路:一个二维费用(bool)背包问题,一个是元素个数(相当于体积),一个是体重。先求出总重的一半和元素个数一半,进行背包。
#include<iostream> #include<cstdio> using namespace std; bool f[101][45001]={false}; int a[101]={0}; int main() { int i,j,sum=0,n,k,q,cha,summ; cin>>n; if (n%2==0) k=n/2; else k=n/2+1; for (i=1;i<=n;++i) { cin>>a[i]; sum+=a[i]; } if (summ%2==0) summ=sum/2; else summ=sum/2+1; for (i=0;i<=1;++i) f[i][0]=true; for (i=1;i<=n;++i) for (j=summ;j>=a[i];--j) for (q=k;q>=1;--q) f[q][j]=f[q][j]||f[q-1][j-a[i]]; for (j=summ;j>=0;--j) if (f[k][j]) { cout<<min(j,sum-j)<<" "<<max(j,sum-j)<<endl; break; } }
CODEVS1060 搞笑世界杯
随着世界杯小组赛的结束,法国,阿根廷等世界强队都纷纷被淘汰,让人心痛不已. 于是有
人组织了一场搞笑世界杯,将这些被淘汰的强队重新组织起来和世界杯一同比赛.你和你的朋
友欣然去购买球票.不过搞笑世界杯的球票出售方式也很特别,它们只准备了两种球票.A 类
票------免费球票 B 类票-------双倍价钱球票.购买时由工作人员通过掷硬币决定,投到正面
的买A类票, 反面的买B类票.并且由于是市场经济,主办方不可能倒贴钱,所以他们总是准备
了同样多的A类票和B类票.你和你的朋友十分幸运的排到了某场精彩比赛的最后两个位置.
这时工作人员开始通过硬币售票.不过更为幸运的是当工作人员到你们面前时他发现已无需
再掷硬币了,因为剩下的这两张票全是免费票。
你和你的朋友在欣喜之余,想计算一下排在队尾的两个人同时拿到一种票的概率是多少
(包括同时拿A 类票或B类票) 假设工作人员准备了2n 张球票,其中n 张A类票,n 张B类票,并且排在队伍中的人每人必须且只能买一张球票(不管掷到的是该买A 还是该买B).
思路:先说一个比较坑爹的事情,读入的是2n,不是n。f[i][j]表示第i人买票时买了j张A票的概率。对于j有三种情况:
1)j=0:f[i][j]=f[i-1][j]*0.5; 2)0<j<n:f[i][j]=(f[i-1][j]+f[i-1][j-1])*0.5; 3)j=n:f[i][j]=f[i-1][j]+f[i-1][j-1]*0.5。
初始化f[0][0]=1;输出f[n*2-2][n]*2(因为最后两张可以全是A或B);
#include<iostream> #include<cstdio> using namespace std; double f[3000][3000]={0}; int main() { int n,i,j; cin>>n; n/=2; f[0][0]=1; for (i=1;i<=2*n;++i) for (j=0;j<=n;++j) { if (j>i) continue; if (j==0) f[i][j]=f[i-1][j]*0.5; else { if (j<n) f[i][j]=(f[i-1][j]+f[i-1][j-1])*0.5; else f[i][j]=f[i-1][j]+f[i-1][j-1]*0.5; } } printf("%.4f\n",f[n*2-2][n]*2); }
CODEVS3369 膜拜
神牛有很多…当然…每个同学都有自己衷心膜拜的神牛.
某学校有两位神牛,神牛甲和神牛乙。新入学的N位同学们早已耳闻他们的神话。所以,已经衷心地膜拜其中一位了。
现在,老师要给他们分机房。
但是,要么保证整个机房都是同一位神牛的膜拜者,或者两个神牛的膜拜者人数差不超过M。
另外,现在N位同学排成一排,老师只会把连续一段的同学分进一个机房。老师想知道,至少需要多少个机房。
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> using namespace std; int f[3000]={0},sum1[3000]={0},sum2[3000]={0}; int main() { int n,m,i,j,x; memset(f,127/3,sizeof(f)); scanf("%d%d",&n,&m); for (i=1;i<=n;++i) { sum1[i]=sum1[i-1]; sum2[i]=sum2[i-1]; scanf("%d",&x); if (x==1) ++sum1[i]; else ++sum2[i]; } f[0]=0; for (i=1;i<=n;++i) for (j=0;j<i;++j) if (abs((sum1[i]-sum1[j])-(sum2[i]-sum2[j]))<=m|| sum1[i]-sum1[j]==0||sum2[i]-sum2[j]==0) f[i]=min(f[i],f[j]+1); cout<<f[n]<<endl; }
CODEVS1491 取物品
现在有n个物品(有可能相同),请您编程计算从中取k个有多少种不同的取法。
思路:多重背包问题,将数字作为元素,出现的次数为个数。注意选取个数时候的循环要循环到1,一开始循环到了0,结果wa了。。。作死。。。
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int a[31],f[31]={0},w[31]={0}; int main() { int n,k,i,j,p; cin>>n>>k; for (i=1;i<=n;++i) cin>>a[i]; sort(a+1,a+n+1); ++w[0]; ++w[w[0]]; for (i=2;i<=n;++i) { if (a[i]==a[i-1]) ++w[w[0]]; else { ++w[0]; ++w[w[0]]; } if (w[w[0]]>k) w[w[0]]=k; } f[0]=1; for (i=1;i<=w[0];++i) for (j=k;j>=0;--j) for (p=w[i];p>=1;--p) if (j>=p) f[j]=f[j]+f[j-p]; cout<<f[k]<<endl; }
小V1032
免费馅饼(NOI98)SERKOI最新推出了一种叫做“免费馅饼”的游戏:游戏在一个舞台上进行。舞台的宽度为W格,天幕的高度为H格,游戏者占一格。开始时游戏者站在舞台的正中央,手里拿着一个托盘。下图为天幕的高度为4格时某一个时刻游戏者接馅饼的情景。
游戏开始后,从舞台天幕顶端的格子中不断出现馅饼并垂直下落。游戏者左右移动去接馅饼。游戏者每秒可以向左或向右移动一格或两格,也可以站在原地不动。
馅饼有很多种,游戏者事先根据自己的口味,对各种馅饼依次打了分。同时,在8-308电脑的遥控下,各种馅饼下落的速度也是不一样的,下落速度以格/秒为单位。
当馅饼在某一秒末恰好到达游戏者所在的格子中,游戏者就收集到了这块馅饼。
写一个程序,帮助我们的游戏者收集馅饼,使得所收集馅饼的分数之和最大。
注意:同等情况下优先走编号小的点。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; int a[1500][1000]={0},f[1500][1000],way[1500][1000]={0}; void work(int t,int x) { if (t>1) work(t-1,way[t][x]); cout<<x-way[t][x]<<endl; } int main() { int w,h,i,j,k,x,t,p,v,e,maxt=0,mat,mai,man=0; cin>>w>>h; while (cin>>t>>x>>v>>p) { if ((h-1)%v==0) e=(h-1)/v+t; else e=(h-1)/v+t+1; a[e][x]=a[e][x]+p; if (e>maxt) maxt=e; } for (i=0;i<=1499;++i) for (j=0;j<=99;++j) f[i][j]=-1000000000; f[0][w/2+1]=0; for (i=1;i<=maxt;++i) for (j=1;j<=w;++j) for (k=-2;k<=2;++k) if (j+k>=1&&j+k<=w) { if (f[i][j]<f[i-1][j+k]+a[i][j]) { f[i][j]=f[i-1][j+k]+a[i][j]; way[i][j]=j+k; } } for (i=1;i<=maxt;++i) for (j=1;j<=w;++j) if (f[i][j]>man) { man=f[i][j]; mat=i; mai=j; } cout<<man<<endl; if (man!=0) work(mat,mai); }
小V1035邮局
一些村庄被建在一条笔直的高速公路边上。我们用一条坐标轴来描述这条高速公路,每一个村庄的坐标都是整数。没有两个村庄坐标相同。两个村庄间的距离,定义为它们坐标值差的绝对值。人们需要在一些村庄建立邮局——当然,并不是每一个村庄都必须建立邮局。邮局必须被建在村庄里,因此它的坐标和它所在的村庄坐标相同。每个村庄使用离它最近的那个邮局,建立这些邮局的原则是:所有村庄到各自所使用的邮局的距离总和最小。
你的任务是编写一个程序,在给定了每个村庄的坐标和将要建立的邮局数之后,按照上述原则,合理地选择这些邮局的位置。
#include<iostream> #include<cstdio> #include<cstring> #include<cmath> using namespace std; int a[500][500]={0},x[500]={0},f[500][100]={0},way[500][100]={0}; void work(int p,int q) { if (q!=1) work(way[p][q],q-1); cout<<x[(way[p][q]+1+p)/2]<<" "; } int main() { int i,j,k,n,m,mid; ios::sync_with_stdio(false); cin>>n>>m; memset(f,127/3,sizeof(f)); for (i=1;i<=n;++i) { cin>>x[i]; } for (i=1;i<=n-1;++i) for (j=i+1;j<=n;++j) { mid=(i+j)/2; for (k=i;k<=j;++k) a[i][j]=a[i][j]+abs(x[k]-x[mid]); } for (i=1;i<=n;++i) f[i][1]=a[1][i]; for (j=2;j<=m;++j) for (i=j+1;i<=n;++i) for (k=j-1;k<=i-1;++k) { if (f[k][j-1]+a[k+1][i]<f[i][j]) { f[i][j]=f[k][j-1]+a[k+1][i]; way[i][j]=k; } } cout<<f[n][m]<<endl; work(n,m); cout<<endl; }
小V1080多人背包
DD 和好朋友们要去爬山啦!他们一共有 K 个人,每个人都会背一个包。这些包的容量是相同的,都是 V。可以装进背包里的一共有 N 种物品,每种物品都有给定的体积和价值。
在 DD 看来,合理的背包安排方案是这样的:
每个人背包里装的物品的总体积恰等于包的容量。
每个包里的每种物品最多只有一件,但两个不同的包中可以存在相同的物品。
任意两个人,他们包里的物品清单不能完全相同。
在满足以上要求的前提下,所有包里的所有物品的总价值最大是多少呢?
思路:可以将k个人看做一个人,就是一个人前k优化方案,原来的一维背包变为二维的,表示体积为i的第j优解,用两个队列更新,每次选两个队列中较大的一个,放入f[i][j]。
#include<cstdio> #include<iostream> using namespace std; int f[5001][51]={0},a[201]={0},w[201]={0},x,y,qx[51]={0},qy[51]={0}; int main() { int k,v,n,i,j,t; long long ans=0; cin>>k>>v>>n; for (i=1;i<=n;++i) cin>>a[i]>>w[i]; for (j=0;j<=v;++j) for (t=0;t<=k;++t) f[j][t]=-210000000; f[0][1]=0; for (i=1;i<=n;++i) for (j=v;j>=a[i];--j) { x=y=1; for (t=1;t<=k;++t) { qx[t]=f[j][t]; qy[t]=f[j-a[i]][t]; } for (t=1;t<=k;++t) { if (qx[x]==-210000000&&qy[y]==-210000000) break; f[j][t]=qx[x]; ++x; if (qy[y]+w[i]>f[j][t]) { --x; f[j][t]=qy[y]+w[i]; ++y; } } } for (i=1;i<=k;++i) ans=ans+f[v][i]; cout<<ans<<endl; }
SCOI2008着色方案
题目大意:有k种不同的颜色,每种颜色有ci种,求相邻涂不同颜色的方案数。
思路:一开始并不知道该怎么写,后来在其他大神的讲解下,才注意到ci<=5,然后听说用六维数组,然后就开始写,发现挂了。。。又听了他们的讲解。发现动态转移中六个变量a、b、c、d、e、i,分别保存剩余一个、两个、三个……五个的染色种类数和上一个涂的颜色是1~5中的哪一个,因为是上一次的,所以我们在下一层比较的时候要-1。然后用记忆化搜索。f[a][b][c][d][e][i]=(a-(i==2))*f[a-1][b][c][d][e][1]+(b-(i==3))*f[a+1][b-1][c][d][e][2]+……e*f[a][b][c][d+1][e-1][5]。
#include<iostream> #include<cstdio> #include<cstring> #define inf 1000000007 using namespace std; long long f[16][16][16][16][16][6]={0}; int ci[16]={0},num[6]={0},k; bool visit[16][16][16][16][16][6]={false}; long long dp(int a,int b,int c,int d,int e,int co) { long long ans=0; int i; if (a<0||b<0||c<0||d<0||e<0) return 0; if (!a&&!b&&!c&&!d&&!e) return 1; if (visit[a][b][c][d][e][co]) return f[a][b][c][d][e][co]; ans=(ans+(a-(co==2))*dp(a-1,b,c,d,e,1))%inf; ans=(ans+(b-(co==3))*dp(a+1,b-1,c,d,e,2))%inf; ans=(ans+(c-(co==4))*dp(a,b+1,c-1,d,e,3))%inf; ans=(ans+(d-(co==5))*dp(a,b,c+1,d-1,e,4))%inf; ans=(ans+e*dp(a,b,c,d+1,e-1,5))%inf; visit[a][b][c][d][e][co]=true; return f[a][b][c][d][e][co]=ans; } int main() { int i,j; scanf("%d",&k); for (i=1;i<=k;++i) { scanf("%d",&ci[i]); ++num[ci[i]]; } printf("%lld\n",dp(num[1],num[2],num[3],num[4],num[5],0)); }
HAOI2007上升序列
题目大意:给定一个序列,求任意长度的上升子序列,要求字典序最小(这里的字典序是位置最小)
思路:用nlogn的做法求最长上升子序列,然后从头往后扫m遍,找后面的值大的同时f数组满足相应条件的值输出。求f数组的时候,用lower_bound wa了,但用upper_bound就ac了。。。
bzoj1019 SHOI汉诺塔
题目大意:给定移动优先级的汉诺塔游戏,求多少步走完。(AB表示从A移到B)
思路:一开始只会暴搜,但是n到30,肯定超时。后来才知道是dp。借鉴普通汉诺塔的思路,我们从一个柱子移到另一个,就是先移动i-1个,然后把最大的移走,然后再把这i-1个移到最大的上面。这里的优先级就要求我们可能要多移动几次柱子,从而满足要求。我们用fi[i][j]表示i上有j个圆盘移走的步数,gi[i][j]表示i上j个圆盘移到的柱子,a为当前的柱子,b为i-1个移到的柱子,c就是第三个柱子,c=3-a-b。我们分情况讨论一下:如果gi[b][i-1]=c,我们只需要把i-1个从a移到b,最大的一个从a上移走,然后把b上的i-1个移到c,这样最后移到了c;如果gi[b][i-1]=a,我们需要把i-1个从a移到b,最大的一个从a上移走,然后把i-1个从b移到a,把最大的一个从c移到b,然后把i-1个从a移到b上就可以了,这样最后移到了b。这样我们就可以写出dp了,同时初始化保证了每个柱子上的圆盘都是按优先级移动,之后的每一步都是满足优先级的。
这道题目题意很明确,同时又是常见模型汉诺塔问题的变化,值得反思。
#include<iostream> #include<cstring> #include<algorithm> #include<cstdio> #define maxnode 35 using namespace std; int move[7][2]={0},gi[3][maxnode]={0}; long long fi[3][maxnode]={0}; int main() { int n,i,j,a,b,c; char ch; scanf("%d",&n); for (i=1;i<=6;++i) for (j=0;j<=1;++j) { while(scanf("%c",&ch)==1){if (ch>='A'&&ch<='C') break;} move[i][j]=ch-'A'; } for (i=0;i<3;++i) for (j=1;j<=6;++j) if (move[j][0]==i) { fi[i][1]=1;gi[i][1]=move[j][1];break; } for (i=2;i<=n;++i) for (a=0;a<3;++a) { b=gi[a][i-1];c=3-a-b; fi[a][i]=fi[a][i-1]+1; if (gi[b][i-1]==c) { fi[a][i]+=fi[b][i-1];gi[a][i]=c; } else { fi[a][i]+=fi[b][i-1]+1+fi[a][i-1];gi[a][i]=b; } } printf("%lld\n",fi[0][n]); }
bzoj1875 HH去散步
题目大意:已知n个点m条边,问从a走k步到b有多少种方案。走的过程中不能往回走。
思路:如果没有往回走这个要求,我们只需要建立n*n的矩阵,对两点之间加边,然后矩乘一下就可以了。加上这个条件,我们就需要换一下矩阵,建立一个2m*2m的矩阵,表示一条边连向下一条边,然后我们对有公共点的并且不是相反边的加给矩阵,然后矩乘k-1次,最后找到从a开始的终点在b的矩阵中的位置加给答案就可以了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define p 45989 using namespace std; struct use{ int num[125][125]; }m1,map; int m,point[25]={0},next[125]={0},st[125]={0},en[125]={0},tot=0; bool f[125][125]={false}; void add(int x,int y) { ++tot;next[tot]=point[x];point[x]=tot;st[tot]=x;en[tot]=y; ++tot;next[tot]=point[y];point[y]=tot;st[tot]=y;en[tot]=x; } use cheng(use m1,use m2) { int i,j,k;use m3; for (i=1;i<=m*2;++i) for (j=1;j<=m*2;++j) { m3.num[i][j]=0; for (k=1;k<=m*2;++k) m3.num[i][j]=(m3.num[i][j]+((m1.num[i][k]*m2.num[k][j])%p))%p; } return m3; } use mi(use m2,int t) { if (t==1) return m2; use m3=mi(m2,t/2); if (t%2==0) return cheng(m3,m3); else return cheng(m3,cheng(m3,m2)); } int main() { int n,t,a,b,i,j,k,ai,bi,ans=0,sum=0; scanf("%d%d%d%d%d",&n,&m,&t,&a,&b);++a;++b; for (i=1;i<=m;++i) { scanf("%d%d",&ai,&bi);++ai;++bi; add(ai,bi);f[i*2-1][i*2]=f[i*2][i*2-1]=true; } for (i=1;i<=m*2;++i) for (j=1;j<=m*2;++j) if (en[i]==st[j]&&!f[i][j]) ++map.num[i][j]; if (t>1) m1=mi(map,t-1); for (j=1;j<=2*m;++j) { sum=0; for (k=1;k<=2*m;++k) sum=(sum+((st[k]==a?1:0)*m1.num[k][j])%p)%p; if (en[j]==b) ans=(ans+sum)%p; } printf("%d\n",ans); }
bzoj1260 涂色
题目大意:给定一个大写字母的序列,每次可以染连续一段同种颜色,求最少几次能把空的序列染出给定序列。
思路:这道题目是一个很简单的dp,但是做的时候却只写了暴搜。这是一个区间dp,从小区间穷举区间后,分情况讨论,如果区间两端相等,就取[i+1,j],[i,j-1],[i+1,j-1]+1中的最小值;如果不一样,就穷举中间断点k,取min([i,k][k+1,j])。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 51 using namespace std; char ss[maxnode]; int f[maxnode][maxnode]={0}; int main() { int n,i,j,k; memset(f,127/3,sizeof(f)); scanf("%s",&ss);n=strlen(ss); for (i=n;i>=1;--i) ss[i]=ss[i-1]; for (i=1;i<=n;++i) f[i][i]=1; for (i=n-1;i>=1;--i) for (j=i+1;j<=n;++j) { if (ss[i]==ss[j]) { f[i][j]=min(f[i+1][j],f[i][j-1]); if (i<j-1) f[i][j]=min(f[i][j],f[i+1][j-1]+1); } else for (k=i;k<j;++k) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]); } printf("%d\n",f[1][n]); }
bzoj1089 严格n元树
题目大意:求深度为d的非叶子节点儿子数均为n的树的个数。
思路:设f[i]表示深度小于等于i的n元树的个数,f[i]=f[i-1]^n+1,最后输出f[d]-f[d-1]就可以了。因为高精度的问题所以用了python。
关于递推式,考虑加入一个根节点,那么它的n个儿子都有f[i-1]种选择,所以有f[i-1]^n种+1(都不选的)。
n,d=map(int, raw_input().split()) if d==0: print 1 else: f=[1] for i in range(0, d+1): f.append(f[i]**n+1) print f[d]-f[d-1]
bzoj1037 生日聚会
题目大意:给m个男孩和n个女孩排队,求任意区间男女之差不超过k个的方案数。
思路:设f[i][j][a][b]表示取i个男孩、j个女孩,且在以i+j为结尾的任意一段中,男比女最多多a个、女比男最多多b个的方案数,考虑最后一个取男或者女,更新答案。最后答案就是f[m][n]的和。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define p 12345678 using namespace std; int f[200][200][25][25]={0}; int main() { int n,m,k,i,j,a,b,ans=0; scanf("%d%d%d",&n,&m,&k); f[0][0][0][0]=1; for (i=0;i<=n;++i) for (j=0;j<=m;++j) for (a=0;a<=k;++a) for (b=0;b<=k;++b) { if (i<n) f[i+1][j][a+1][max(b-1,0)]=(f[i+1][j][a+1][max(b-1,0)]+f[i][j][a][b])%p; if (j<m) f[i][j+1][max(a-1,0)][b+1]=(f[i][j+1][max(a-1,0)][b+1]+f[i][j][a][b])%p; } for (a=0;a<=k;++a) for (b=0;b<=k;++b) ans=(ans+f[n][m][a][b])%p; printf("%d\n",ans); }
bzoj1025 游戏(!!!)
题目大意:给定n个数字,它们可以通过置换最终回到原数列,记次数为xi,求所有可能的次数。
思路:相当于求x1+x2+...+xi=n的lcm(x1,x2,...,xi)的种类数。我们只需要关注分解质因数后相应指数最大的就可以了。可以证明两两互质的时候(相当于判断一些lcm是不是符合,对lcm唯一分解之后的和最小,如果<n,后面补1;如果=n,一定可以,所以只需要求<=n的就行了,也就是每个数都是pi^ai且pi不同时)不会重复,所以f[i][j]表示用前i个质数和为j的方案数,dp转移一下就可以了,f[i][j]=f[i-1][j]+sigma(t=1~...)f[i-1][j-pi^t]。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 1500 #define LL long long using namespace std; int prime[maxnode]={0}; LL f[maxnode][maxnode]={0}; bool flag[maxnode]={false}; void shai(int n) { int i,j; for (i=2;i<=n;++i) { if (!flag[i]) prime[++prime[0]]=i; for (j=1;j<=prime[0]&&i*prime[j]<=n;++j) { flag[i*prime[j]]=true; if (i%prime[j]==0) break; } } } int main() { int n,i,j,t,mi;LL ans=0; scanf("%d",&n); shai(n);f[0][0]=1; for (i=1;i<=prime[0];++i) for (j=0;j<=n;++j) { f[i][j]=f[i-1][j]; for (mi=prime[i];mi<=j;mi*=prime[i]) f[i][j]+=f[i-1][j-mi]; } for (i=0;i<=n;++i) ans+=f[prime[0]][i]; printf("%I64d\n",ans); }
bzoj2298 problem a
题目大意:给定一些人的描述(ai个人的分比我高,bi个人的分比我低),判断最少几个人说谎(可能同分)。
思路:对于每个人建立一个[bi+1,n-ai]的线段,对于每一个这样的线段它的权值是min(区间长度,完全相同的区间个数),那么就转化成了求端点处也不能重复的线段覆盖最大权值问题,用n-f[n]就可以了。注意这里的取min()、右端点在左端点左边时要忽略。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 100005 using namespace std; struct use{ int li,ri,va; }edge[maxnode]={0},ai[maxnode]={0}; int f[maxnode]={0}; int cmp(const use &x,const use &y){return (x.li==y.li ? x.ri<y.ri : x.li<y.li);} int cmp2(const use &x,const use &y){return x.ri<y.ri;} int main() { int n,i,j,aa,bb,tt=0,tot=0; scanf("%d",&n); for (i=1;i<=n;++i) { scanf("%d%d",&aa,&bb); if (aa+bb+1>n) continue; edge[++tt].li=bb+1;edge[tt].ri=n-aa; } sort(edge+1,edge+tt+1,cmp); for (i=1;i<=tt;++i) { j=i; while(j<n&&edge[j].li==edge[j+1].li&&edge[j].ri==edge[j+1].ri) ++j; ai[++tot].li=edge[i].li;ai[tot].ri=edge[i].ri; ai[tot].va=min(edge[i].ri-edge[i].li+1,j-i+1);i=j; } sort(ai+1,ai+tot+1,cmp2);j=1; for (i=1;i<=n;++i) { f[i]=f[i-1]; while(j<=tot&&ai[j].ri==i) { f[i]=max(f[i],f[ai[j].li-1]+ai[j].va);++j; } } printf("%d\n",n-f[n]); }
bzoj2431 逆序对数列
题目大意:给定n、k,求1~n排列中逆序对有k个的数量。
思路:f[i][j]表示前i个数有j个逆序对的种类,f[i][j]=sigma(k=0~min(j,i-1))f[i-1][j-k],这样是O(n^3),如果我们对第二维做个前缀和就可以是O(n^2)了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 2005 #define p 10000 using namespace std; int f[maxnode][maxnode]={0},sum[maxnode][maxnode]={0}; int main() { int n,k,i,j,t,up; scanf("%d%d",&n,&k);f[1][0]=1; for (i=0;i<=k;++i) sum[1][i]=1; for (i=2;i<=n;++i) for (j=0;j<=k;++j) { up=min(j,i-1); f[i][j]=((sum[i-1][j]-sum[i-1][j-up-1])%p+p)%p; sum[i][j]=(sum[i][j-1]+f[i][j])%p; } printf("%d\n",f[n][k]); }
bzoj1197 花仙子的魔法
题目大意:求施放m次魔法后n维空间中最多有几种花(每次施放魔法,都会以任意一个点为中心半径为r施放,与中心距离小于等于r的点这个二进制位上为1,否则为0)。
思路:设f[i][j]表示i维空间j次魔法。考虑新加入一个n维的空间,原来就有f[i][j-1]种了,可以划分出来的新的区域为相交空间(i-1维)、j-1个i-1维空间的分割方案,所以f[i][j]=f[i-1][j-1]+f[i][j-1]。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long using namespace std; LL f[20][155]={0}; int main() { int i,j,n,m;scanf("%d%d",&n,&m); for (i=1;i<=n;++i) f[1][i]=2*i; for (i=2;i<=m;++i) { f[i][1]=2; for (j=2;j<=n;++j) f[i][j]=f[i][j-1]+f[i-1][j-1]; } printf("%I64d\n",f[m][n]); }
bzoj4247 挂饰
题目大意:一开始你有一个挂钩,然后有n个挂饰,每个挂饰有一个价值和挂钩数量,求最大价值。
思路:f[i][j]表示取到第i个物品有j个空挂钩的最大价值。这样的话j可能到n^2,复杂度就是n^3,会tle,但是我们发现j取到n就可以了,不需要再往下取(不可能挂那么多个挂饰),所以复杂度为n^2,注意每次更新过来的时候,都是上一次的至少从1更新过来(不能在0个挂钩上挂东西),所以取个max(详见code)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 2005 using namespace std; struct use{ int ai,bi; }wu[maxnode]={0}; int cmp(const use &x,const use &y){return x.ai>y.ai;} int fi[maxnode][maxnode]={0}; int main() { int n,m=0,i,j,ans=0;scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d%d",&wu[i].ai,&wu[i].bi); memset(fi,128,sizeof(fi));fi[0][1]=0; sort(wu+1,wu+n+1,cmp); for (i=1;i<=n;++i) for (j=0;j<=n;++j) fi[i][j]=max(fi[i-1][j],fi[i-1][min(n,max(0,j-wu[i].ai)+1)]+wu[i].bi); for (i=0;i<=n;++i) ans=max(ans,fi[n][i]); printf("%d\n",ans); }
bzoj3594 方伯伯的玉米田
题目大意:给定一个序列,可以最多进行k次对一个区间+1的操作,问最后最少拔掉多少使剩下的为不下降。
思路:f[i][j]表示前i个j次提升(以i结尾)的最长的序列,f[i][j]=f[a][b]+1(a=0~i-1,b=0~j,ai[i]+j>=ai[a]+b),然后要用二维树状数组优化一下,第一维是高度,第二维是几次提升,注意这里dp更新的时候循环的顺序,不要让当前i的状态再去更新状态i。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; int cc[6001][601]={0},ai[10005]={0}; int lowbit(int x){return x&(-x);} int ask(int x,int y) { int ans=0; for (int i=x;i;i-=lowbit(i)) for (int j=y;j;j-=lowbit(j)) ans=max(ans,cc[i][j]); return ans; } void ins(int x,int y,int v) { for (int i=x;i<=6000;i+=lowbit(i)) for (int j=y;j<=600;j+=lowbit(j)) cc[i][j]=max(cc[i][j],v); } int main() { int i,j,n,k,ci;scanf("%d%d",&n,&k); for (i=1;i<=n;++i) scanf("%d",&ai[i]); for (i=1;i<=n;++i) for (j=k;j>=0;--j){ ci=ask(ai[i]+j,j+1)+1;ins(ai[i]+j,j+1,ci); }printf("%d\n",ask(6000,600)); }
codevs1258 关路灯
题目大意:给定一排上的n个路灯,到最左点的距离和它们的功率,一个人从给定的某个路灯旁开始关灯,速度1m/s,求关完所有灯消耗的最小能量。
思路:fi[i][j][0]表示关上i~j的路灯停在左边的最小能量,fi[i][j][1]表示关上i~j的路灯停在右边的最小能量,gi[i][j]表示除i~j外的其它灯的功率和。然后区间dp更新。注意更新中从一边到另一边的距离和相应的功率。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxnode 1005 #define inf 1050000000LL using namespace std; struct use{int di,wi;}ai[maxnode]={0}; int fi[maxnode][maxnode][2]={0},gi[maxnode][maxnode]={0}; int cmp(const use&x,const use&y){return x.di<y.di;} int main() { int n,v,i,j,l,r,sum=0;scanf("%d%d",&n,&v); for (i=1;i<=n;++i) for (j=1;j<=n;++j) fi[i][j][0]=fi[i][j][1]=inf; fi[v][v][0]=fi[v][v][1]=0; for (i=1;i<=n;++i){scanf("%d%d",&ai[i].di,&ai[i].wi);sum+=ai[i].wi;} sort(ai+1,ai+n+1,cmp); for (i=1;i<=n;++i){ gi[i][i]=sum-ai[i].wi; for (j=i+1;j<=n;++j) gi[i][j]=gi[i][j-1]-ai[j].wi; } for (i=2;i<=n;++i){ for (l=1;l<=n-i+1;++l){ r=l+i-1; fi[l][r][0]=min(fi[l+1][r][0]+gi[l+1][r]*(ai[l+1].di-ai[l].di), min(fi[l+1][r][1]+gi[l+1][r]*(ai[r].di-ai[l].di), min(fi[l][r-1][0]+gi[l][r-1]*(ai[r].di-ai[l].di)+gi[l][r]*(ai[r].di-ai[l].di), fi[l][r-1][1]+gi[l][r-1]*(ai[r].di-ai[r-1].di)+gi[l][r]*(ai[r].di-ai[l].di)))); fi[l][r][1]=min(fi[l+1][r][0]+gi[l+1][r]*(ai[l+1].di-ai[l].di)+gi[l][r]*(ai[r].di-ai[l].di), min(fi[l+1][r][1]+gi[l+1][r]*(ai[r].di-ai[l].di)+gi[l][r]*(ai[r].di-ai[l].di), min(fi[l][r-1][0]+gi[l][r-1]*(ai[r].di-ai[l].di), fi[l][r-1][1]+gi[l][r-1]*(ai[r].di-ai[r-1].di)))); } }printf("%d\n",min(fi[1][n][0],fi[1][n][1])); }
bzoj2660 最多的方案
题目大意:求把一个数用不同的Fibonacci数分解的不同方案。
思路:先把这个数分解成尽量靠右的Fibonacci数的和,因为fi[i]=fi[i-1]+fi[i-2],所以如果把选那些Fibonacci数压成01串的话,001和110的和是一样的,所以我们考虑一个10000001的串,它的和也是10000110、10011010、11101010的和,我们会发现多出来的这三种情况正好是这个1和上个1之间长度的一半。设dp[i][0/1]表示第i个贪心出来的Fibonacci数的下标选或者不选,那么dp[i][1]=dp[i-1][0]+dp[i-1][1],dp[i][0]=(ff[i]-ff[i-1])/2*dp[i-1][0]+(ff[i]-ff[i-1]-1)/2*dp[i-1][1],最后答案就是dp[][0]+dp[][1]。(注意Fibonacci数列问题的求解大多都要回归的递推式上!!!)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long using namespace std; LL fi[200]={0},dp[200][2]={0}; int ff[200]={0}; int main(){ int i,j;LL n;scanf("%I64d",&n); fi[1]=1;fi[2]=2; for (fi[0]=3;fi[fi[0]-1]<=n;++fi[0]) fi[fi[0]]=fi[fi[0]-1]+fi[fi[0]-2]; --fi[0];for (i=fi[0];i;--i) if (n>=fi[i]){ff[++ff[0]]=i;n-=fi[i];} for (i=1;i<=ff[0]/2;++i) swap(ff[i],ff[ff[0]+1-i]); dp[0][1]=1;j=ff[0];ff[0]=0; for (i=1;i<=j;++i){ dp[i][1]=dp[i-1][0]+dp[i-1][1]; dp[i][0]=(ff[i]-ff[i-1])/2*dp[i-1][0]+(ff[i]-ff[i-1]-1)/2*dp[i-1][1]; }printf("%I64d\n",dp[j][0]+dp[j][1]); }
bzoj2302 problem c(!!!)
题目大意:给定n个人,每个人一个编号,从1~n排座,先到编号处,如果有人就往后顺延,给定m个人的确定编号,求有多少种编号序列。
思路:先考虑无解,m个确定编号中相应编号的后缀和sum[i],如果比能安排的位置(n-i+1)多就是无解。然后考虑dp,fi[i][j]表示第i个人后面(包括第i个人)安排好了j个人,fi[i][j]=sigma(k=0~j)fi[i+1][j-k]*ci[j][k],考虑第i个人的确定能使k个人确定,j的循环范围就是最多能确定的人数n-i+1-sum[i],最后答案就是fi[1][n-m]。(这种dp太神了!!!)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 305 #define LL long long using namespace std; int sum[maxm]={0},n,ai[maxm]={0}; LL fi[maxm][maxm],ci[maxm][maxm]={0}; bool judge(){ int i,j; for (i=n;i;--i){ sum[i]+=sum[i+1]; if (sum[i]>n-i+1) return false; }return true; } int main(){ int t,m,i,j,k,p,u,v;scanf("%d",&t); while(t--){ scanf("%d%d%d",&n,&m,&p);memset(sum,0,sizeof(sum)); memset(fi,0,sizeof(fi));memset(ai,0,sizeof(ai)); for (i=1;i<=m;++i){scanf("%d%d",&u,&v);++sum[v];} if (!judge()) printf("NO\n"); else{ for (i=0;i<=n;++i){ ci[i][0]=ci[i][i]=1; for (j=1;j<i;++j) ci[i][j]=(ci[i-1][j-1]+ci[i-1][j])%p; }fi[n+1][0]=1; for (i=n;i;--i) for (j=0;j<=n-i+1-sum[i];++j) for (k=0;k<=j;++k) fi[i][j]=(fi[i][j]+fi[i+1][j-k]*ci[j][k]%p)%p; printf("YES %I64d\n",fi[1][n-m]); } } }
bzoj1190 梦幻宝珠(!!!)
题目大意:01背包,只是每个物品的体积为a*2^b(a<=10,b<=30)的形式。
思路:非常神的dp方法。考虑根据b的不同来分层,设fi[i][j]表示j*2^i+(w&(1<<i-1))的体积下最大价值,每一i层内部都是普通的背包,但是不同层之间是可以更新的,fi[i][j]=max(fi[i][j],fi[i][j-k]+fi[i-1][min(k*2+((w>>i-1)&1),1000)]),这里的j-k表示当前层用的体积,这个地方可以把fi[][]表示的意义都写出来进行理解,也可以举个栗子。这里的1000是可以用一个数组表示出更精确的上界的,但是为了省事,所以就直接用1000了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define LL long long #define up 1000 using namespace std; LL fi[50][1010]; int main(){ int n,w,i,j,a,b,x,k;LL ans; while(scanf("%d%d",&n,&w)==2){ if (n==-1&&w==-1) break; memset(fi,0,sizeof(fi)); for (i=1;i<=n;++i){ scanf("%d%d",&a,&x);b=0; while(~a&1){a>>=1;++b;} for (j=up;j>=a;--j) fi[b][j]=max(fi[b][j],fi[b][j-a]+(LL)x); }ans=0; for (i=0;i<=up;++i) ans=max(ans,fi[0][i]); for (i=1;i<=30&&(1<<i)<=w;++i) for (j=min(up,w>>i);j>=0;--j){ for (k=0;k<=j;++k) fi[i][j]=max(fi[i][j],fi[i][j-k]+fi[i-1][min(k*2+((w>>i-1)&1),up)]); ans=max(ans,fi[i][j]); }printf("%I64d\n",ans); } }
bzoj1296 粉刷匠
题目大意:给定n块长度为m的木板,要求刷上0、1两种颜色,每次可以选择一个连续区间粉刷某种颜色,每个方块最多粉刷一次,求最多能有多少个方格符合要求。
思路:对每一块单独考虑(最后分组背包一下就行了),设fi[i][j]表示终点到i粉刷j次的最多符合的,fi[i][j]=max(fi[k][j-1]+max(sum[i]-sum[k],i-k-sum[i]+sum[k])),sum[i]表示到i的1的个数。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 100 #define maxe 3000 using namespace std; int sum[maxm]={0},fi[maxm][maxm]={0},dp[maxe]={0}; char ss[maxm]; int main(){ int i,j,n,m,k,q,u,v,t;scanf("%d%d%d",&n,&m,&t); for (i=1;i<=n;++i){ scanf("%s",&ss);memset(sum,0,sizeof(sum)); memset(fi,0,sizeof(fi)); for (j=0;j<m;++j) sum[j+1]=sum[j]+ss[j]-'0'; for (u=1;u<=m;++u) for (j=1;j<=m;++j) for (k=0;k<j;++k){ v=sum[j]-sum[k]; fi[j][u]=max(fi[j][u],fi[k][u-1]+max(v,j-k-v)); } for (j=t;j;--j) for (k=min(j,m);k;--k) dp[j]=max(dp[j],dp[j-k]+fi[m][k]); }printf("%d\n",dp[t]); }
bzoj1084 最大子矩阵
题目大意:给定一个n*m的子矩阵(最大是100×2的),求其中k个子矩阵(互不重合)和最大。
思路:因为数据范围很小,所以一开始考虑状压dp,后来发现直接更新好像比状压还好写,所以就分m的1、2进行dp。m=1的时候就是选k段子段和最大;m=2的时候,fi[i][j][t]表示第一行到i、第二行到j、选出t个子矩阵的最大和,然后转移更新一下就可以了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 105 using namespace std; int si[3][maxm]={0},fi[maxm][maxm][20]={0}; int main(){ int n,m,k,i,j,t,p,up;scanf("%d%d%d",&n,&m,&k); if (m==1){ for (i=1;i<=n;++i){scanf("%d",&si[1][i]);si[1][i]+=si[1][i-1];} for (i=1;i<=n;++i) for (up=min(k,i),t=1;t<=up;++t) for (j=t-1;j<i;++j) fi[i][i][t]=max(fi[i][i][t],max(fi[j][j][t],fi[j][j][t-1]+si[1][i]-si[1][j])); }else{ for (i=1;i<=n;++i){ scanf("%d%d",&si[1][i],&si[2][i]); si[1][i]+=si[1][i-1];si[2][i]+=si[2][i-1]; }for (i=1;i<=n;++i) for (j=1;j<=n;++j){ if (i==j){ up=min(i,k); for (t=1;t<=up;++t) for (p=t-1;p<i;++p) fi[i][j][t]=max(fi[i][j][t],max(fi[p][p][t], fi[p][p][t-1]+si[1][i]-si[1][p]+si[2][j]-si[2][p])); }for (up=min(i,k),t=1;t<=up;++t) for (p=t-1;p<i;++p) fi[i][j][t]=max(fi[i][j][t],max(fi[p][j][t],fi[p][j][t-1]+si[1][i]-si[1][p])); for (up=min(j,k),t=1;t<=up;++t) for (p=t-1;p<j;++p) fi[i][j][t]=max(fi[i][j][t],max(fi[i][p][t],fi[i][p][t-1]+si[2][j]-si[2][p])); } }printf("%d\n",fi[n][n][k]); }
bzoj2734 集合选数
题目大意:给定n,从1~n的整数中选出一些子集要求x存在则2x、3x均不存在,求集合个数(空集也算)。
思路:考虑一个数x,做一个向左是*3、向下是*2的不完全的矩阵,那么问题就变成了求选矩阵中不相邻元素的方案数,状压dp一下就可以了。注意这个x要从1~n穷举一下,如果没有访问过就要做一遍。
注意:如果有清数组的话,一定不能开太大,防止tle。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 100005 #define p 1000000001 #define LL long long using namespace std; int cnt[maxm][20]={0},tot=0,fi[18][2050]; bool visit[maxm]={false}; void pre(int n){ int i,j,x,y; for (i=1;i<=n;++i){ if (visit[i]) continue; ++tot; for (x=i,j=1;x<=n;++j,x*=2){ ++cnt[tot][0];visit[x]=true; for (y=x;y<=n;y*=3){++cnt[tot][j];visit[y]=true;} } } } int main(){ int n,i,j,t,k,up,up2,ans,sum; scanf("%d",&n);pre(n);ans=1; for (i=1;i<=tot;++i){ fi[0][0]=1;sum=0; for (j=1;j<=cnt[i][0];++j){ up=(1<<cnt[i][j==1? 1 : j-1])-1; up2=(1<<cnt[i][j])-1; for (t=up2;t>=0;--t) fi[j][t]=0; for (t=up;t>=0;--t){ if (t&(t<<1)) continue; for (k=up-t;k;k=(up-t)&(k-1)){ if (k&(k<<1)) continue; if (k>up2) continue; fi[j][k]=(fi[j][k]+fi[j-1][t])%p; }fi[j][0]=(fi[j][0]+fi[j-1][t])%p; } }for (j=(1<<cnt[i][cnt[i][0]])-1;j>=0;--j) sum=(sum+fi[cnt[i][0]][j])%p; ans=(int)((LL)ans*(LL)sum%(LL)p); }printf("%d\n",ans); }
bzoj1801 中国象棋
题目大意:求n*m的棋盘中放炮的互不攻击的方案数。
思路:如果其中一个不超过8,我们可以状压dp。因为炮的特性,所以一行一列中最多有两个炮,所以我们可以设fi[i][j][k]表示第i行有j列为一个、k列为2个,然后考虑第i行放0、1、2个炮,1个的又分为从0到1和从1到2;2个的又分从00到11、从11到22、从01到12。分别转移,最后第n列的所有情况就是答案。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 105 #define LL long long #define p 9999973 using namespace std; LL fi[maxm][maxm][maxm]={0}; LL c(int x){return (LL)x*(LL)(x-1)/2%p;} int main(){ int n,m,i,j,k;LL ans=0; scanf("%d%d",&n,&m);if (n<m) swap(n,m); fi[0][0][0]=1; for (i=1;i<=n;++i) for (j=0;j<=m;++j) for (k=0;k<=m-j;++k){ fi[i][j][k]=fi[i-1][j][k]; if (j) fi[i][j][k]=(fi[i][j][k]+fi[i-1][j-1][k]*(LL)(m-j-k+1)%p)%p; if (k) fi[i][j][k]=(fi[i][j][k]+fi[i-1][j+1][k-1]*(LL)(j+1)%p)%p; if (j>=2) fi[i][j][k]=(fi[i][j][k]+fi[i-1][j-2][k]*c(m-j-k+2)%p)%p; if (k>=2) fi[i][j][k]=(fi[i][j][k]+fi[i-1][j+2][k-2]*c(j+2)%p)%p; if (k&&j) fi[i][j][k]=(fi[i][j][k]+fi[i-1][j][k-1]*(LL)j%p*(LL)(m-j-k+1)%p)%p; if (i==n) ans=(ans+fi[i][j][k])%p; }printf("%I64d\n",ans); }
bzoj1222 产品加工
题目大意:给定n个产品和他们在A、B、AB上加工的时间。求加工完所有的最小时间。
思路:fi[i]表示在A上加工i时间B上最少的时间,滚动数组更新一下就可以了。注意状态的设计。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 30005 using namespace std; int ti[maxm][3],fi[2][maxm]; int main(){ int i,j,n,minn,cur=0,sta=0,ans;scanf("%d",&n); memset(fi,127/3,sizeof(fi));ans=fi[0][0]; for (i=1;i<=n;++i){ minn=maxm; for (j=0;j<3;++j){ scanf("%d",&ti[i][j]); ti[i][j]=(!ti[i][j] ? fi[0][0] : ti[i][j]); minn=min(minn,ti[i][j]); }sta+=minn; }fi[0][0]=0; for (i=1;i<=n;++i){ cur^=1; for (j=sta;j>=0;--j){ fi[cur][j]=ans; fi[cur][j]=min(fi[cur][j],fi[cur^1][j]+ti[i][1]); if (j>=ti[i][0]) fi[cur][j]=min(fi[cur][j],fi[cur^1][j-ti[i][0]]); if (j>=ti[i][2]) fi[cur][j]=min(fi[cur][j],fi[cur^1][j-ti[i][2]]+ti[i][2]); } }for (i=0;i<=sta;++i) ans=min(ans,max(i,fi[cur][i])); printf("%d\n",ans); }
bzoj1090 字符串折叠
题目大意:给定一个字符串和折叠的定义(连续的重复一段可以缩成x(s)的形式,X是重复次数,s是重复子段),求它最短折叠长度。
思路:区间dp,枚举中间断点,但如果后面是前面重复的,就可以再更新。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 105 using namespace std; int fi[maxm][maxm]; char ss[maxm]; bool judge(int x1,int y1,int x2,int y2){ int i,j; if ((y2-x2+1)%(y1-x1+1)) return false; for (j=x1,i=x2;i<=y2;++i,j=(j==y1?x1:j+1)) if (ss[j]!=ss[i]) return false; return true; } int calc(int x){ int i=0; while(x){++i;x/=10;} return i; } int main(){ int i,j,k,l,u,v;scanf("%s",ss+1); l=strlen(ss+1); for (k=1;k<=l;++k) for (u=1;u+k-1<=l;++u){ v=u+k-1;fi[u][v]=k; for (i=u;i<v;++i){ fi[u][v]=min(fi[u][v],fi[u][i]+fi[i+1][v]); if (judge(u,i,i+1,v)) fi[u][v]=min(fi[u][v],fi[u][i]+2+calc(k/(i-u+1))); } }printf("%d\n",fi[1][l]); }
poj1390 blocks(!!!)
题目大意:消方块游戏,消去同颜色长度为x的区间得分为x^2,求最多得分。
思路:可以把相同颜色的区间合并作为色块,len[i]为长度,color[i]为颜色,设fi[i][j][k]表示i到j个色块后面还有k个和j色块颜色相同的块长。1)l=r时,就是sqr(len[j]+k);2)如果j和后面的合并,就是fi[i][j][k]=fi[i][j-1][0]+sqr(len[j]+k)^2;3)如果j和i~j间某一段合并,就是fi[i][j][k]=fi[i][s][k+len[j]]+fi[s+1][j-1][0]。
注意:dp状态的设计要考虑到所有可能情况。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 205 using namespace std; int fi[maxm][maxm][maxm],ai[maxm],color[maxm],len[maxm],cnt; int sqr(int x){return x*x;} int dp(int l,int r,int kk){ if (fi[l][r][kk]) return fi[l][r][kk]; if (l==r) return fi[l][r][kk]=sqr(len[l]+kk); fi[l][r][kk]=dp(l,r-1,0)+sqr(len[r]+kk); for (int i=l;i<r-1;++i) if (color[i]==color[r]) fi[l][r][kk]=max(fi[l][r][kk],dp(l,i,kk+len[r])+dp(i+1,r-1,0)); return fi[l][r][kk]; } int main(){ int n,i,j,t,x;scanf("%d",&t); for (x=1;x<=t;++x){ scanf("%d",&n);memset(fi,0,sizeof(fi)); for (i=1;i<=n;++i) scanf("%d",&ai[i]); color[cnt=1]=ai[1];len[1]=1; for (i=2;i<=n;++i){ if (ai[i]==ai[i-1]) ++len[cnt]; else{color[++cnt]=ai[i];len[cnt]=1;} }printf("Case %d: %d\n",x,dp(1,cnt,0)); } }
poj3612 Telephone Wire
题目大意:给定一个序列,可以对一个位置+x,代价为x^2;序列的最终代价还要加上相邻元素差的绝对值*c,求最小代价。
思路:高度<=100,所以有一种比较暴力的dp可以做到1e9的复杂度,但要寻求优化。fi[i][j]=min(fi[i-1][k]+|j-k|c+(j-ai[i])^2),化简之后就有1)j>=k时,fi[i][j]=(j-ai[i])^2+jc+min(-kc+fi[i-1][k]);2)j<k时,fi[i][j]=(j-ai[i])^2-jc+min(kc+fi[i-1][k])。所以我们可以用两个数组维护min里面的东西,然后转移的时候就少一个100了。
注意:对dp方程的化简可以优化复杂度。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 105 #define maxx 100005 using namespace std; int fi[2][maxm],ai[maxx]={0},gi[maxm][2]; int sqr(int x){return x*x;} int main(){ int n,c,i,j,ans,cur; while(scanf("%d%d",&n,&c)==2){ for (i=1;i<=n;++i) scanf("%d",&ai[i]); cur=0;memset(fi[cur],127/3,sizeof(fi[cur])); ans=fi[cur][0]; for (i=ai[1];i<=100;++i) fi[cur][i]=sqr(i-ai[1]); for (i=2;i<=n;++i){ cur^=1;memset(fi[cur],127/3,sizeof(fi[cur])); memset(gi,127/3,sizeof(gi)); for (j=100;j;--j) gi[j][0]=min(gi[j+1][0],fi[cur^1][j]+j*c); for (j=1;j<=100;++j) gi[j][1]=min(gi[j-1][1],fi[cur^1][j]-j*c); for (j=ai[i];j<=100;++j) fi[cur][j]=sqr(j-ai[i])+min(gi[j][0]-j*c,gi[j][1]+j*c); }for (i=1;i<=100;++i) ans=min(ans,fi[cur][i]); printf("%d\n",ans); } }
hdu5564 Clarke and digits
题目大意:求7的倍数,长度在l~r之间,相邻两位之和不为k的数的个数。
思路:fi[i][j][k]表示i位选j余数为k的方案数,然后用矩阵优化一下。注意矩阵的初始。有一维是前缀和,和的转移是可以从0开始的,但是第一位不可以是0。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define up 10 #define si 7 #define maxm 75 #define p 1000000007 #define LL long long using namespace std; struct use{ LL num[maxm][maxm]; }ji,lm; int idx(int x,int y){return 10*y+x+1;} use cheng(use a,use b){ use c;int i,j,k; for (i=1;i<maxm;++i) for (j=1;j<maxm;++j){ c.num[i][j]=0LL; for (k=1;k<maxm;++k) c.num[i][j]=(c.num[i][j]+a.num[i][k]*b.num[k][j]%p)%p; }return c; } use mi(use a,int x){ if (x==1) return a; use mm=mi(a,x/2); if (x%2) return cheng(a,cheng(mm,mm)); else return cheng(mm,mm); } int main(){ int t,i,j,a,b,l,r,k;LL ans; scanf("%d",&t); while(t--){ scanf("%d%d%d",&l,&r,&k); memset(ji.num,0,sizeof(ji.num)); memset(lm.num,0,sizeof(lm.num)); for (i=1;i<up;++i) lm.num[1][idx(i,i%si)]=1LL; for (i=0;i<up;++i) for (j=0;j<up;++j){ if (i+j==k) continue; for (a=0;a<si;++a) ji.num[idx(i,a)][idx(j,(a*10+j)%si)]=1LL; }ji.num[71][71]=1LL; for (i=0;i<up;++i) ji.num[idx(i,0)][71]=1LL; use ci=cheng(lm,mi(ji,r));ans=ci.num[1][71]; if (l>1){ ci=cheng(lm,mi(ji,l-1)); ans=((ans-ci.num[1][71])%p+p)%p; }printf("%I64d\n",ans); } }
bzoj1996 合唱队
题目大意:给定一个序列,从前往后,如果这个数小于之前的数就插入到队列的左边,如果大于就插入到右边。现给定最后的队列,求一开始有多少种序列满足要求。
思路:插入队列可以看做从队列里取,就是从左右两边取,所以fi[i][j][k]表示i~j的区间选(k=0左k=1右)时的方案数,只需要比较这个区间左右端点和能更新过来的区间左右端点的大小就可以了,因为长度为1的区间只计入一次答案,所以fi[i][i][0]=1,fi[i][i][1]=0就可以了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define maxm 1005 #define LL long long #define p 19650827 using namespace std; LL fi[maxm][maxm][2]={0LL}; int ai[maxm]; void jia(LL &x,LL y){x=(x+y)%p;} int main(){ int n,i,j,k;scanf("%d",&n); for (i=1;i<=n;++i) scanf("%d",&ai[i]); for (i=1;i<=n;++i) fi[i][i][0]=1LL; for (k=2;k<=n;++k) for (i=1;i+k-1<=n;++i){ j=i+k-1; if (ai[i]<ai[i+1]) jia(fi[i][j][0],fi[i+1][j][0]); if (ai[i]<ai[j]) jia(fi[i][j][0],fi[i+1][j][1]); if (ai[j]>ai[i]) jia(fi[i][j][1],fi[i][j-1][0]); if (ai[j]>ai[j-1]) jia(fi[i][j][1],fi[i][j-1][1]); }printf("%I64d\n",(fi[1][n][0]+fi[1][n][1])%p); }
bzoj4347 Nim z utrudnieniem
题目大意:给定n堆石子,从中取出d的倍数堆石子后进行nim游戏,问使后手必胜的取堆数的方案数。
思路:就是求取出d的倍数堆石子后余下的nim和为0的方案数,所以fi[i][j][k]表示前i堆取j(%d)堆石子余下的nim和为k的方案数,排序之后保证了k会单增,不会因为ai而去更新极大值,因为sigma ai[i]=m,所以总的复杂度就是md。这题卡内存,滚动数组也不能解决,所以要找新的方法。可以发现fi[i][j][k]的转移中只用到了fi[i-1][j-1][k]+fi[i-1][j][k^ai[i]],后一项是与当前j有关的,而这项的转移和被转移项的转移是可以同时更新且只有这个位置更新的,所以可以一起转移,要特殊处理0的时候(再开一个数组记录一下)。注意:如果n是d的倍数的时候要-1(一开始不能全取完)。
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #define p 1000000007 #define N 500005 #define up 1050000 using namespace std; int ai[N],fi[10][up]={0},gi[up]; int main(){ int n,i,j,k,d,uu,x; scanf("%d%d",&n,&d); for (i=1;i<=n;++i) scanf("%d",&ai[i]); sort(ai+1,ai+n+1);fi[0][0]=1; for (i=1;i<=n;++i){ uu=1;while(uu<=ai[i]) uu<<=1; for (k=0;k<uu;++k) gi[k]=(fi[d-1][k]+fi[0][k^ai[i]])%p; for (j=d-1;j;--j) for (k=0;k<uu;++k){ if (k>(k^ai[i])) continue; x=fi[j][k]; fi[j][k]=(fi[j-1][k]+fi[j][k^ai[i]])%p; fi[j][k^ai[i]]=(fi[j-1][k^ai[i]]+x)%p; }for (k=0;k<uu;++k) fi[0][k]=gi[k]; }printf("%d\n",(fi[0][0]-(n%d==0)+p)%p); }
bzoj2423 最长公共子序列
题目大意:给定两个字符串求最长公共子序列的长度和个数。
思路:dp维护很简单,但是要注意个数的时候:如果fi[i][j]=fi[i-1][j-1],ij的方案里面要减去i-1j-1的方案。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 5005 #define p 100000000 using namespace std; int fi[2][N]={0},gi[2][N]={0}; char s1[N],s2[N]; char in(){ char ch=getchar(); while((ch<'A'||ch>'Z')&&ch!='.') ch=getchar(); return ch;} void jia(int &x,int y){x=(x+y)%p;} void jian(int &x,int y){x=((x-y)%p+p)%p;} int main(){ int n,i,j,l1=0,l2=0,cur=0;char ch; while((ch=in())!='.') s1[++l1]=ch; while((ch=in())!='.') s2[++l2]=ch; for (i=0;i<=l2;++i) gi[cur][i]=1; for (i=1;i<=l1;++i){ memset(fi[cur^=1],0,sizeof(fi[cur])); memset(gi[cur],0,sizeof(gi[cur])); gi[cur][0]=1; for (j=1;j<=l2;++j){ if (fi[cur^1][j]>=fi[cur][j]){ if (fi[cur^1][j]==fi[cur][j]) jia(gi[cur][j],gi[cur^1][j]); else{fi[cur][j]=fi[cur^1][j];gi[cur][j]=gi[cur^1][j];} }if (fi[cur][j-1]>=fi[cur][j]){ if (fi[cur][j-1]==fi[cur][j]) jia(gi[cur][j],gi[cur][j-1]); else{fi[cur][j]=fi[cur][j-1];gi[cur][j]=gi[cur][j-1];} }if (s1[i]==s2[j]&&fi[cur^1][j-1]+1>=fi[cur][j]){ if (fi[cur^1][j-1]+1==fi[cur][j]) jia(gi[cur][j],gi[cur^1][j-1]); else{fi[cur][j]=fi[cur^1][j-1]+1;gi[cur][j]=gi[cur^1][j-1];} }if (fi[cur][j]==fi[cur^1][j-1]) jian(gi[cur][j],gi[cur^1][j-1]); } }printf("%d\n%d\n",fi[cur][l2],gi[cur][l2]); }
bzoj3612 平衡
题目大意:给定一个2n+1的跷跷板,支点在n+1处,每个位置有一块质量相同的橡皮,求取走k块后仍然平衡的方案数。
思路:fi[i][j]表示i分成j份(所有数互不相同,并且最大数不超过n),fi[i][j]=fi[i-j][j-1](最小的是1)+fi[i-j][j](最小的不是1)-fi[i-n-1][j-1](最大的是n+1,因为方案中最大的是n,+1后最大只可能是n+1为不合法的)。然后枚举左右选的数和数的和累计答案就行了,注意答案中可能包含中间的点,所以要分k和k-1两种(两边还可能都不选)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 100005 #define K 15 using namespace std; int fi[N][K]; int main(){ int i,j,t,n,k,p,up,ans;scanf("%d",&t); while(t--){ memset(fi,0,sizeof(fi)); scanf("%d%d%d",&n,&k,&p); up=n*k;fi[0][0]=1;ans=0; for (i=1;i<=up;++i) for (j=1;j<=min(i,k);++j){ fi[i][j]=(fi[i-j][j-1]+fi[i-j][j])%p; if (i>=n+1) fi[i][j]=((fi[i][j]-fi[i-n-1][j-1])%p+p)%p;} for (i=0;i<=k;++i) for (j=0;j<=up;++j){ ans=(ans+fi[j][i]*fi[j][k-i]%p)%p; if (i<k) ans=(ans+fi[j][i]*fi[j][k-1-i]%p)%p; }printf("%d\n",ans); } }
(如果把i分成j份,数可以相同的话,转移是fi[i][j]=fi[i-1][j-1]+fi[i-j][j])
bzoj1560 火星藏宝图
题目大意:给定一个m*m的网格,有n个小岛,已知每个小岛的坐标和价值,从x->y的代价是sqr(x.x-y.x)+sqr(x.y-y.y),只能向右下(包括右和下)走,求从(1,1)->(m.m)的价值-代价的最大值。
思路:可以发现每次转移的时候只需要转移到x这个点右下方最靠近这个点的那些点就可以了(纵坐标是一个不减的序列),所以可以先以横坐标为第一关键字、纵坐标为第二关键字排序,记录一下所有点的序号和这一列的开始点的序号;然后纵坐标第一关键字、横坐标第二关键字,从1~n扫一遍,进行更新(这样保证了只更新每一列在这个点纵坐标下面的点,虽然不能保证单调,但是也不会出现错误。)
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 200005 #define M 1005 using namespace std; struct use{int x,y,v,po;}ai[N]; int point[M]={0},fi[N],bi[N]={0}; int cmp1(const use&x,const use&y){return (x.x==y.x ? x.y<y.y : x.x<y.x);} int cmp2(const use&x,const use&y){return (x.y==y.y ? x.x<y.x : x.y<y.y);} int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();} return x;} int sqr(int x){return x*x;} int main(){ int i,j,k,n,m;n=in();m=in(); for (i=1;i<=n;++i) ai[i]=(use){in(),in(),in()}; sort(ai+1,ai+n+1,cmp1); for (i=1;i<=n;++i) ai[i].po=i; for (i=1;i<=n;i=j+1){ j=i;k=ai[i].x;point[k]=i; while(ai[j+1].x==k) ++j; }point[m+1]=n+1; memset(fi,-60,sizeof(fi)); sort(ai+1,ai+n+1,cmp2); for (i=1;i<=n;++i) bi[ai[i].po]=i; for (fi[1]=ai[1].v,i=1;i<=n;++i){ ++point[ai[i].x]; for (j=ai[i].x;j<=m;++j) if (ai[bi[point[j]]].x==j){ k=bi[point[j]]; fi[k]=max(fi[k],fi[i]-(sqr(ai[i].x-ai[k].x)+sqr(ai[i].y-ai[k].y))+ai[k].v); } }printf("%d\n",fi[n]); }
bzoj1226 学校食堂
题目大意:给定n个人,每个人最多允许紧跟在他之后的bi个人先取,一个人取的时间是和在他之前取得人的(ai|aj)-(ai&aj),求n个人取完饭之后最少时间。
思路:状压。fi[i][j][k]表示前i-1个人都取了,i和i之后7个人的情况是j(从前到后是从低位到高位),上一个取的人和i的相对位置是k的最小时间。转移非常的麻烦,如果i这个人取了,可以转移到fi[i+1][j>>1][k-1];然后考虑i和i之后7个人谁吃,从前往后扫这8个人,同时要维护还没吃饭的人所能允许的最后位置。最后答案就是fi[n+1][0][-8~-1]中的最小值。
注意:k的循环范围是从-8~7,因为-8那个人的时候取i-1是可以允许的。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 1010 #define up 8 #define inf 2100000000 using namespace std; int fi[N][1<<up][20],bi[N]={0},ti[N]={0},ci[N]={0}; int calc(int x,int y){return (x<=0 ? 0 : (ti[x]|ti[y])-(ti[x]&ti[y]));} int main(){ int n,i,j,k,p,t,mn,uu;scanf("%d",&t); for (i=0;i<(1<<up);++i) for (ci[i]=0,j=i;j;j>>=1,++ci[0]); while(t--){ memset(fi,127/3,sizeof(fi)); scanf("%d",&n);uu=1<<8; for (i=1;i<=n;++i) scanf("%d%d",&ti[i],&bi[i]); fi[1][0][up-1]=0; for(i=1;i<=n;++i) for (j=0;j<uu;++j) for (k=-8;k<=7;++k){ if (fi[i][j][k+up]==fi[0][0][0]) continue; if (j&1) fi[i+1][j>>1][k-1+up]=min(fi[i][j][k+up],fi[i+1][j>>1][k-1+up]); else for (mn=2*n,p=0;p<=7;++p){ if (j&(1<<p)) continue; if (p+i>mn) break; mn=min(mn,p+i+bi[p+i]); fi[i][j|(1<<p)][p+up]=min(fi[i][j|(1<<p)][p+up], fi[i][j][k+up]+calc(k+i,p+i)); } } for (mn=inf,i=-8;i<0;++i) mn=min(mn,fi[n+1][0][i+up]); printf("%d\n",mn);} }
bzoj3233 找硬币(!!!)
题目大意:给定n个物品,构造硬币:x1,x2,x3...(xb是xa的倍数,b>a),求购买n个物品(不找零)的最小硬币数。
思路:尽量选大的硬币,pre[i]表示n个物品最多用硬币i多少个,fi[i]表示最多用硬币i,小于的都符合要求的最小硬币数,fi[i]=min(fi[j]-(i/j-1)*pre[i])(j|i)。根n枚举约数的话可以在bzoj上过掉,但是还有一些优化:可以像筛法那样,只从fi[i/k](k是i的质因数)转移(化简一下式子,发现如果不是质因数的时候,是没有质因数的时候更优的),所以可以预先处理出每个数的质因数(线筛的时候处理一下)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 55 #define M 100005 using namespace std; int fi[M],ai[N],pre[M]={0},pr[M][N],prime[M]={0},point[M],next[M],en[M],tot=0; bool flag[M]={false}; void build(int n){ int i,j,k; for (i=2;i<=n;++i){ if (!flag[i]){ prime[++prime[0]]=i; next[++tot]=point[i];point[i]=tot;en[tot]=i; }for (j=1;j<=prime[0]&&i*prime[j]<=n;++j){ flag[k=i*prime[j]]=true; if (i%prime[j]==0){ point[k]=point[i];break; }else{ next[++tot]=point[i];point[k]=tot;en[tot]=prime[j]; } } } } int main(){ int i,j,k,n,m=0,cnt;scanf("%d",&n); for (cnt=0,i=1;i<=n;++i){ scanf("%d",&ai[i]); cnt+=ai[i];m=max(m,ai[i]); }memset(fi,127/3,sizeof(fi)); build(m);fi[1]=cnt; for (i=1;i<=m;++i) for (j=1;j<=n;++j) pre[i]+=ai[j]/i; for (i=2;i<=m;++i){ for (j=point[i];j;j=next[j]){ k=en[j]; fi[i]=min(fi[i],fi[i/k]-(k-1)*pre[i]); }cnt=min(cnt,fi[i]); }printf("%d\n",cnt); }
bzoj4416 阶乘字符串
题目大意:给定一个由前n个字母组成的字符串S,问由n个字母组成的n!个字符串是否都是S的字串(不一定连续)。
思路:我们可以构造一个n^2的字符串使得满足条件,所以|S|的长度至少为n^2,因为|S|长度为450,所以n至多为21。设fi[i]表示选中i这些字母使得满足阶乘字符串的最大位置最小,状压更新一下,如果最后答案<=|S|就可以。
注意:预处理每个位置的下一个字母所在位置的时候,next[|S|+1][]=|S|+1。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #define N 500 #define mz 26 using namespace std; char ss[N]; int next[N][mz],la[N],fi[1<<21]={0}; int main(){ int n,i,j,k,cnt,ll,mx;scanf("%d",&k); while(k--){ scanf("%d%s",&n,ss+1); ll=strlen(ss+1); if (n>21){printf("NO\n");continue;} memset(fi,0,sizeof(fi)); for (i=0;i<n;++i){la[i]=next[ll+1][i]=ll+1;} for (i=ll;i>=0;--i){ for (j=0;j<n;++j) next[i][j]=la[j]; if (i) la[ss[i]-'a']=i; }for (i=1;i<(1<<n);++i) for (j=0;j<n;++j) if ((1<<j)&i) fi[i]=max(fi[i],next[fi[(1<<j)^i]][j]); if (fi[(1<<n)-1]<=ll) printf("YES\n"); else printf("NO\n"); } }
bzoj3810 Stanovi
题目大意:给定一个n*m的矩阵,要求分割成一些矩形a、b,每个矩形要有一条边在边界上,代价是sigma (ab-k)^2,求最小代价。
思路:fi[i][j][a][b][c][d]表示长为i宽为j的四周是否在边上的最小代价,记忆化搜索。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 305 #define LL long long #define inf 1000000000000000000LL using namespace std; LL fi[N][N][2][2][2][2],kk; bool vi[N][N][2][2][2][2]={false}; inline LL sqr(LL x){return (kk-x)*(kk-x);} inline LL dp(int n,int m,int a,int b,int c,int d){ if (n>m){ swap(n,m);swap(a,c);swap(b,d); }if (a>b) swap(a,b); if (c>d) swap(c,d); if (fi[n][m][a][b][c][d]!=-1) return fi[n][m][a][b][c][d]; int i;LL ans; if (a+b+c+d==0) return fi[n][m][a][b][c][d]=inf; ans=sqr((LL)(n*m)); if (a+b+c>0&&a+b+d>0) for (i=1;i<n;++i) ans=min(ans,dp(i,m,a,b,c,0)+dp(n-i,m,a,b,0,d)); if (c+d+a>0&&c+d+b>0) for (i=1;i<m;++i) ans=min(ans,dp(n,i,a,0,c,d)+dp(n,m-i,0,b,c,d)); return fi[n][m][a][b][c][d]=ans;} int main(){ int n,m;scanf("%d%d%I64d",&n,&m,&kk); memset(fi,-1,sizeof(fi)); printf("%I64d\n",dp(n,m,1,1,1,1)); }
bzoj1151 动物园
题目大意:一个n大小的环,m个人,每个人可以看到长度为5的区间,同时知道每个人讨厌和喜欢这个区间中的那些位置,当满足:(1)这个人能看到的区间中至少有一个他喜欢的;(2)这个人看到的区间中至少有一个他讨厌的被移走了,中任意一个条件的时候这个人高兴。问怎么安排n个位置中的东西是否移走能使的高兴的人最多。
思路:如果是一条链,状压dp是比较好写的,fi[i][j]表示到第i位,j是压最后5位的状态,最多高兴的人数。如果成环,可以枚举前4位的状态,然后一直做到n+4位,n+1~n+4的状态和枚举的状态一致就可以了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 50005 #define up 5 using namespace std; struct use{int e,f,l,fi,li;}ai[N]; int fi[N][1<<up]={0}; bool judge(int k,int x){return ((k&ai[x].li)||((((1<<up)-1)^k)&ai[x].fi));} int main(){ int n,m,i,j,k,kk,p,x,cnt,la,ans=0,inf; scanf("%d%d",&n,&m); memset(ai,0,sizeof(ai)); for (i=1;i<=m;++i){ scanf("%d%d%d",&ai[i].e,&ai[i].f,&ai[i].l); for (j=1;j<=ai[i].f;++j){ scanf("%d",&x); if (x<ai[i].e) x+=n; ai[i].fi|=(1<<(ai[i].e+4-x)); }for (j=1;j<=ai[i].l;++j){ scanf("%d",&x); if (x<ai[i].e) x+=n; ai[i].li|=(1<<(ai[i].e+4-x)); } }for (i=0;i<(1<<4);++i){ memset(fi,-60,sizeof(fi)); fi[4][i]=0;inf=-fi[0][0]; for (x=1,j=5;j<=n+4;++j){ for (k=0;k<(1<<up);++k){ if (fi[j-1][k]==-inf) continue; for (p=0;p<=1;++p){ if (j>n&&(p^((i>>(4-j+n))&1))) continue; kk=((k<<1)|p)&((1<<5)-1); for (cnt=0,la=x;la<=m&&ai[la].e+4==j;++la) if (judge(kk,la)) ++cnt; fi[j][kk]=max(fi[j][kk],fi[j-1][k]+cnt); } }for (;x<=m&&ai[x].e+4==j;++x); }for (j=0;j<(1<<up);++j) ans=max(ans,fi[n+4][j]); } printf("%d\n",ans); }
bzoj3195 奇怪的道路
题目大意:n个城市,m条道路,已知k,每条道路连接的两点满足1<=|u-v|<=k,问有多少种连路方式。(n,m<=30,k<=8)
思路:状压k+1个城市的状态,从这k+1个中的第一个往后连边,这样转移会tle。所以改变一下状态,fi[i][j][a][b]表示到第i位,选了j条边,状态a,从i这个位置连出去的a状态下上一条边的位置(!!!),如果没有最后一维可能导致同一个方案算多遍。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 35 #define up 512 #define p 1000000007 using namespace std; int fi[2][N][up][N]={0}; void add(int &x,int y){x=(x+y)%p;} int main(){ int i,j,a,b,c,n,m,k,uu,ans=0,cur=0; scanf("%d%d%d",&n,&m,&k); fi[0][0][0][0]=1;uu=1<<(k+1); for (i=1;i<=n;++i){ cur^=1;memset(fi[cur],0,sizeof(fi[cur])); for (j=0;j<=m;++j) for (a=0;a<uu;++a){ if (a&1) continue; for (b=0;b<=k;++b) add(fi[cur][j][a>>1][0],fi[cur^1][j][a][b]); } for (j=0;j<m;++j) for (a=0;a<uu;++a) for (b=0;b<=k;++b){ if (!fi[cur][j][a][b]) continue; for (c=min(n-i,k);c>=b&&c;--c){ add(fi[cur][j+1][a^1^(1<<c)][c],fi[cur][j][a][b]); } } }for (i=0;i<=k;++i) add(ans,fi[cur][m][0][i]); printf("%d\n",ans); }
SDOI2010地精部落
题目大意:求长度为n的抖动子序列的种类数。
公式: / 0 (i<=0||i>n||j<=0||j>i)
f[i][j]=|
\ f[i][j-1]+f[i-1][i-j] (i>0&&i<=n&&j>0&&j<=i)
(f[i][j]意义同下)
思路:抖动子序列里面有一个很好的性质:对于等位区段上升和下降的一一对应。且位置上的数如果用1...n表示的话,就是对称的。
我们用f[i][j]表示长度为i开头是j的第一段下降的种类数。初始化f[2][2]=1。
分情况:1)如果我们第二位取j-1,那么就是长度为i-1,开头是j-1的第一段上升序列的种类数,根据之前的性质,就可以把它转化为以(i-1)-(j-1)+1开头的长度为i-1的第一段下降的序列,也就是f[i-1][i-j+1];
2)如果我们第二位不取j-1,那么就是f[i][j-1],(这个比较好理解,就是相当于前缀和,f[i][1]+f[i][2]+...+f[i][j-2],因为我们一直是这样累加上来的,所以就是f[i][j-1]),如果证明的话就。。。
那么我们得到f[i][j]=f[i][j-1]+f[i-1][i-j+1],这样f[n+1][n+1]*2就是答案了(之所以是n+1,因为我们把第一位看做n+1,第二位才能放1~n的数,这样我们得到的相当于长度为n开头1~n的第一段上升的序列,根据性质,*2就是答案了。)
关于公式里面是f[n][n]代表答案的,其实就是把f[i][j]=f[i+1][j+1]了,那么中间的[i-j+1]也就变成了[i-j],初始化也变成了f[1][1]=1。
因为可能卡内存,所以我们用滚动数组,比较巧妙也很简单。
#include<iostream> #include<cstdio> using namespace std; int f[2][5000]={0}; int main() { freopen("sdoi10goblin.in","r",stdin); freopen("sdoi10goblin.out","w",stdout); int i,j,kind,n,p; scanf("%d%d",&n,&p); f[0][2]=1; for (i=3;i<=n+1;++i) { kind=i&1; for (j=1;j<=i;++j) { f[kind][j]=(f[kind][j-1]+f[!kind][i-j+1])%p; } } printf("%d\n",(2*f[kind][n+1])%p); fclose(stdin); fclose(stdout); }
bzoj1786&&1831 Pair 配对
题目大意:给定一个1~k的长度为n的序列,有一些位置上的数不知道(1~k任选),问最少逆序对的个数。
思路:任选的数是单调不减的(如果前面大,换了之后会更优,任选的数之间没有逆序对),fi[i][j]表示到第i位数最后<=j,最小逆序对,预处理gi[i][j]表示i~n小于j的已知数的个数,更新fi的时候处理前i-1位大于j的个数cnt[j]。答案就是已知数的逆序对+fi[la][k](la表示最后一个任选的位置)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 10005 #define M 105 using namespace std; int fi[N][M],ai[N],cnt[M]={0},gi[N][M]={0}; int main(){ int i,j,n,k,la,ans=0,mn; scanf("%d%d",&n,&k); for (i=1;i<=n;++i) scanf("%d",&ai[i]); memset(fi,127/3,sizeof(fi));mn=fi[0][0]; memset(fi[0],0,sizeof(fi[0])); for (i=n;i;--i){ for (j=1;j<=k;++j) gi[i][j]=gi[i+1][j]; if (ai[i]>0) for (j=ai[i]+1;j<=k;++j) ++gi[i][j]; }for (la=0,i=1;i<=n;++i){ if (ai[i]>0){ for (j=ai[i]-1;j;--j) ++cnt[j]; ans+=cnt[ai[i]]; }else{ for (j=1;j<=k;++j) fi[i][j]=min(fi[i][j-1],fi[la][j]+cnt[j]+gi[i][j]); la=i; } }printf("%d\n",ans+fi[la][k]); }
bzoj4509 Angry Cows
题目大意:一排草堆,可以发射一头奶牛,半径为R以内的可以爆炸,第i次被引爆的草堆还会爆炸,半径是R-i,问所有草堆引爆的最小半径是多少。
思路:设fi[i]表示i及以前的都引爆的最小半径,gi[i]表示i及以后的都引爆的最小半径。fi为例,fi[i]=min(ai-aj,fi[j+1]+1),其中ai-aj>fi[j]+1,这个可以单调的求,gi同理。最后算答案的时候要注意,不一定是在相邻两个中间放并只炸这两个,可以炸附近的多个(!!!),所以要把ij作为指针一个从头一个从尾做一遍,取min。对于可能出现0.5的情况,可以*2来解决。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 50005 #define inf 2100000000 #define LD double using namespace std; int ai[N],fi[N],gi[N]; int main(){ int n,i,j,ans=inf+2;scanf("%d",&n); for (i=1;i<=n;++i){ scanf("%d",&ai[i]);ai[i]<<=1; fi[i]=gi[i]=inf; }sort(ai+1,ai+n+1); fi[1]=-2;gi[n]=-2; for (j=1,i=2;i<=n;++i){ while(i-j>1&&ai[i]-ai[j+1]>fi[j+1]+2) ++j; fi[i]=min(ai[i]-ai[j],fi[j+1]+2); }for (j=n,i=n-1;i;--i){ while(j-i>1&&ai[j-1]-ai[i]>gi[j-1]+2) --j; gi[i]=min(ai[j]-ai[i],gi[j-1]+2); }for (i=1,j=n;i<j;){ ans=min(ans,max((ai[j]-ai[i])>>1,max(fi[i],gi[j])+2)); if (fi[i+1]<gi[j-1]) ++i; else --j; }printf("%.1f\n",(LD)ans/2.); }
bzoj4584 赛艇
题目大意:n个学校,每个学校可以派出ai~bi的船队也可以不派,如果派出要求i派出的要大于前面每个学校派出的,求方案数。
思路:把ai和bi+1放入离散数组,离散之后,形成O(n)个段,每个学校的对应一些连续的段。设fi[i][j][k]表示前i个学校最多到j且j的有k个学校的方案数,转移的时候可以O(1)转移,这样会mle,可以忽略i那一维,倒着循环。因为k=1的时候转移要用到前面的所有状态,所以用gs[j]表示前i个<=j的所有方案,转移的时候用gi表示i这一层的,就可以从gs[i-1]转移过来了。转移fi的时候可以不算j的组合数,这样方便计算(!!)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 505 #define M 1005 #define p 1000000007 #define LL long long using namespace std; struct use{int l,r;}ai[N]; int ci[M],cz=0,fi[M][N],gi[M],gs[M],n,c[M][M],fac[M],dd; inline int add(int a,int b){dd=a+b;return (dd>=p ? dd-p : dd);} inline int mul(int a,int b){return (int)((LL)a*(LL)b%p);} inline int mi(int x,int y){ int a=1; for (;y;y>>=1){ if (y&1) a=mul(a,x); x=mul(x,x); }return a;} int main(){ int i,j,k,l,r,cur,ans=0,la,cc;scanf("%d",&n); for (i=1;i<=n;++i){ scanf("%d%d",&l,&r); ai[i]=(use){l,r}; ci[++cz]=l;ci[++cz]=r+1; fac[i]=mi(i,p-2); }sort(ci+1,ci+cz+1); cz=unique(ci+1,ci+cz+1)-ci-1; for (i=1;i<cz;++i){ k=ci[i+1]-ci[i];c[i][0]=1; for (j=1;j<=n&&j<=k;++j) c[i][j]=mul(c[i][j-1],mul(k-j+1,fac[j])); }cur=0;la=1; for (i=0;i<cz;++i) gs[i]=1; for (i=1;i<=n;++i){ cur^=1;la^=1; l=upper_bound(ci+1,ci+cz+1,ai[i].l)-ci-1; r=upper_bound(ci+1,ci+cz+1,ai[i].r)-ci-1; memset(gi,0,sizeof(gi)); for (j=l;j<=r;++j){ for (k=i;k>=2;--k){ cc=fi[j][k-1]; gi[j]=add(gi[j],mul(cc,c[j][k])); fi[j][k]=add(fi[j][k],cc); }cc=gs[j-1]; gi[j]=add(gi[j],mul(cc,c[j][1])); fi[j][1]=add(fi[j][1],cc); }for (j=l;j<cz;++j){ gi[j]=add(gi[j],gi[j-1]); gs[j]=add(gi[j],gs[j]); } }for (i=1;i<cz;++i) for (j=1;j<=n;++j) ans=add(ans,mul(fi[i][j],c[i][j])); printf("%d\n",ans); }
一开始写的是fi[i][j]表示到i,最多到j这一段的方案数,预处理j这一段从连续x个里面选的时候的方案数,转移的时候枚举j的到哪,会在预处理的时候tle。
bzoj4498 魔法的碰撞
题目大意:把n个魔法师放在长度为l的一条格子上,每个有权值d[i],要求相邻两个的距离>=max(d[i],d[i-1]),问方案数。
思路:考虑n个魔法师占了x个格的方案数有f,就会给答案贡献f*c(l-x+n,n)(在n个魔法师的n+1个空中插入l-x个格子)。考虑怎么求f,按d从大到小插入的话,如果知道有几个空位就比较好转移。设fi[i][j][k]表示前i个魔法师占了j个格子有k个能放的方案数,转移的时候可以在空格子的地方放一个魔法师并且不再占格子,转移到fi[i+1][j][k-1](有k个位置可以选,*k);占出一个空位,转移到fi[i+1][j+d][k](有k个位置可以选,每个位置有两个方向可以留格子,*2k);占出两个空位,转移到fi[i+1][j+2d][k+1](有k个位置可以选,*k)。
注意:从大到小的dp,提前计算代价(!!)。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 1000005 #define M 45 #define nn 3205 #define p 1000000007LL #define LL long long using namespace std; int di[M]; LL fac[N],inv[N],fi[M][nn][M]; LL mi(LL x,LL y){ LL a=1LL; for (;y;y>>=1){ if (y&1LL) a=a*x%p; x=x*x%p; }return a;} LL getc(int n,int m){return fac[n]*inv[m]%p*inv[n-m]%p;} void add(LL &x,LL y){x=(x+y)%p;} int cmp(const int&x,const int&y){return x>y;} int main(){ int l,n,i,j,k,d,up;LL ans=0LL;scanf("%d%d",&l,&n); for (i=1;i<=n;++i) scanf("%d",&di[i]); sort(di+1,di+n+1,cmp); for (fac[0]=1LL,i=1;i<=l;++i) fac[i]=fac[i-1]*(LL)i%p; inv[l]=mi(fac[l],p-2LL); for (i=l-1;i>=0;--i) inv[i]=inv[i+1]*(LL)(i+1)%p; memset(fi,0,sizeof(fi)); d=di[1];up=min(80*n,l); fi[1][1][0]=fi[1][d<<1|1][2]=1LL; fi[1][d+1][1]=2LL; for (i=1;i<n;++i){ d=di[i+1]; for (j=1;j<=up;++j) for (k=0;k<=n;++k){ if (!fi[i][j][k]) continue; add(fi[i+1][j+d*2][k+1],fi[i][j][k]*k%p); if (k) add(fi[i+1][j][k-1],fi[i][j][k]*k%p); add(fi[i+1][j+d][k],fi[i][j][k]*k*2LL%p); } }for (j=1;j<=up;++j) add(ans,fi[n][j][0]*getc(l-j+n,n)%p); printf("%I64d\n",ans); }
省队集训 rabbits
题目大意:三只兔子一开始在a,b,c,最终在x,y,z,可以选两个兔子x、y,变成2x-y,x的位置,但一次跳跃不能跳过两只兔子,问有多少种k次跳跃能完成的方法。
思路:类似跳跳棋,可以知道状态是一个二叉树,相当于求k步从树上一点跳到另一点的方案数,fi[k][i][j]表示还可以跳k步,当前点的深度是i,当前点和目标点的lca深度是j(!!!),可以O(1)转移。一开始处理出初始点和目标点在树上的深度和lca的深度,因为k很小,所以暴力找就可以了。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 105 #define M 3 #define LL long long #define p 1000000007 using namespace std; struct use{ LL nm[M]; bool operator==(const use&x)const{ for (int i=0;i<M;++i) if (nm[i]!=x.nm[i]) return false; return true; } bool bert(){return nm[1]-nm[0]==nm[2]-nm[1];} }ai,bi,st[N]; LL fi[N][N<<1][N<<1]; int kk,bd,sz=0; use gnt(use x){ use y; if (x.nm[1]-x.nm[0]>x.nm[2]-x.nm[1]){ y.nm[0]=x.nm[0];y.nm[2]=x.nm[1]; y.nm[1]=x.nm[1]*2LL-x.nm[2]; }else{ y.nm[0]=x.nm[1];y.nm[2]=x.nm[2]; y.nm[1]=x.nm[1]*2LL-x.nm[0]; }return y;} int find(use x){ for (int i=1;i<=sz;++i) if (x==st[i]) return i; return 0;} void pre(){ use a;int i,j;a=ai; while(true){ st[++sz]=a; if (a.bert()||sz==kk+1) break; a=gnt(a); }a=bi;i=0; while(!(j=find(a))){++i;a=gnt(a);} bd=sz-j+1+i; memset(fi,0,sizeof(fi)); fi[kk][sz][sz-j+1]=1LL; } void add(LL &x,LL y){x=(x+y)%p;} void dp(){ int i,j,k;LL ff; for (i=kk;i;--i) for (j=1;j<=kk-i+sz;++j) for (k=1;k<=j;++k){ if (!(ff=fi[i][j][k])) continue; if (j==k){ if (k!=1) add(fi[i-1][j-1][k-1],ff); add(fi[i-1][j+1][k],ff); add(fi[i-1][j+1][min(bd,k+1)],ff); }else{ add(fi[i-1][j+1][k],ff*2LL); add(fi[i-1][j-1][k],ff); } } } int main(){ freopen("rabbits.in","r",stdin); freopen("rabbits.out","w",stdout); int i; for (i=0;i<M;++i) scanf("%I64d",&ai.nm[i]); for (i=0;i<M;++i) scanf("%I64d",&bi.nm[i]); scanf("%d",&kk);pre();dp(); printf("%I64d\n",fi[0][bd][bd]); }
bzoj2121 字符串游戏(!!!)
题目大意:有一个字符串L和字符串集S,如果S中某个字符串出现在L中,就可以把这个在L中删掉,L左右拼起来,问最后最少剩几个字符。
思路:预处理gi[l][r][a][b]表示l~r这一段能否匹配到a这个串的b这个位置,hi[l][r]表示l~r这一段能否消掉,相应的比较好转移(gi的转移有两种情况:1)考虑r
这个位置的匹配b这个字母;2)l~r中间某个位置c匹配到b且c+1~r可以消掉。hi在gi[l][r][a][b]中的b==len[a]的时候可以转移。)然后再用一个区间dp,fi[l][r]表示l~r这一段最少能消到几个字符。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 155 #define M 35 using namespace std; bool gi[N][N][M][M],hi[N][N]; int fi[N][N],len[M]; char li[N],si[M][N]; int main(){ int i,j,a,b,c,l,r,n,ll; scanf("%s%d",li+1,&n); ll=strlen(li+1); for (i=1;i<=n;++i){ scanf("%s",si[i]+1); len[i]=strlen(si[i]+1); }memset(gi,false,sizeof(gi)); memset(hi,false,sizeof(hi)); memset(fi,127/3,sizeof(fi)); for (i=1;i<=ll;++i) for (j=1;j<=n;++j) gi[i][i-1][j][0]=true; for (i=1;i<=ll;++i) for (l=1;l+i-1<=ll;++l){ r=l+i-1; for (a=1;a<=n;++a) for (b=1;b<=len[a];++b){ if (gi[l][r-1][a][b-1]&&li[r]==si[a][b]){ gi[l][r][a][b]=true; if (b==len[a]) hi[l][r]=true; }else{ for (c=l;c<r;++c) if (gi[l][c][a][b]&&hi[c+1][r]) break; if (c<r){ gi[l][r][a][b]=true; if (b==len[a]) hi[l][r]=true; } } } } for (i=1;i<=ll;++i) fi[i][i]=(!hi[i][i]); for (i=2;i<=ll;++i) for (l=1;l+i-1<=ll;++l){ r=l+i-1; if (hi[l][r]){fi[l][r]=0;continue;} for (a=l;a<r;++a) fi[l][r]=min(fi[l][r],fi[l][a]+fi[a+1][r]); }printf("%d\n",fi[1][ll]); }
bzoj4565 字符合并
题目大意:长度为n的01串,每次可以将kk个字符合并成一个,已知合并成的字符和权值,求能获得的最大权值。
思路:每一段肯定是合并成(len-1)%(kk-1)+1(len=k-1的时候是不能合并的)的时候最优,fi[i][j][k]表示i~j这一段合并成k的权值,每次转移,可以看做是在i~l这一段后面加一个字符,后面这一段的长度肯定是kk-1。这样的区间dp一定可以通过一定的方式更新出所有可能的合并方法。
注意:后面那一段的长度一定kk-1才能保证前后k的含义是合理的。
#include<iostream> #include<cstring> #include<cstdio> #include<algorithm> #define N 305 #define M 260 #define LL long long using namespace std; int in(){ char ch=getchar(); while(ch<'0'||ch>'9') ch=getchar(); return ch-'0';} LL fi[N][N][M],wi[N]; int ai[N],bi[N]; void add(LL &x,LL y){if (x<y) x=y;} int main(){ int n,kk,i,l,r,k,a,b,uu,lu; LL ans=0LL;scanf("%d%d",&n,&kk); memset(fi,-60,sizeof(fi)); for (i=1;i<=n;++i) ai[i]=in(); for (i=0;i<(1<<kk);++i){ bi[i]=in();scanf("%I64d",&wi[i]); }for (i=1;i<=n;++i) fi[i][i][ai[i]]=0; for (i=2;i<=n;++i){ uu=(i-1)%(kk-1)+1; for (l=1;l+i-1<=n;++l){ r=l+i-1; for (k=r-1;k>=l;k-=kk-1){ lu=(uu==1 ? (1<<(kk-1)) : (1<<uu)); for (a=0;a<lu;++a){ if (fi[l][k][a]<0) continue; for (b=0;b<2;++b){ if (uu>1) add(fi[l][r][(a<<1)|b],fi[l][k][a]+fi[k+1][r][b]); else add(fi[l][r][bi[(a<<1)|b]],fi[l][k][a]+fi[k+1][r][b]+wi[(a<<1)|b]); } } } } }for (i=0;i<(1<<kk);++i) ans=max(ans,fi[1][n][i]); printf("%I64d\n",ans); }
bzoj4621 Tc605(!)
题目大意:给出一个长度为n的排列,最多k此操作,每次操作可以把一个子序列变成子序列里面的最大值,问最终有多少种可能的数字序列。
思路:fi[i][j]表示到i操作j次的方案数。每次可以不操作:fi[i][j]=fi[i-1][j];操作:因为可能出现这个数改变了其他数,这个数自己又会被之后或者之前的数改变,所以找出这个数能改变的边界,对于这些位置都可以转移,fi[i][j]=sigma(k=l-1~i)fi[k][j-1]。注意在转移的时候,转移类似背包,所以两个转移要一起写,倒序循环。转移的时候会给fi[i][k+1]的位置转移fi[i-1][k],应该减去。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 505 #define p 1000000007 using namespace std; int ai[N],fi[N][N]; void add(int &x,int y){x+=y;if (x>=p) x-=p;} int main(){ int n,kk,i,j,k,l,r,ans=0,sm;scanf("%d%d",&n,&kk); for (i=1;i<=n;++i) scanf("%d",&ai[i]); memset(fi,0,sizeof(fi)); fi[0][0]=1; for (i=1;i<=n;++i){ for (l=i;l>1&&ai[l-1]<ai[i];--l); for (r=i;r<n&&ai[r+1]<ai[i];++r); add(fi[i][kk],fi[i-1][kk]); for (k=kk-1;k>=0;--k){ sm=fi[l-1][k]; for (j=l;j<=r;++j){ add(fi[j][k+1],sm); add(sm,fi[j][k]); }add(fi[i][k],fi[i-1][k]); add(fi[i][k+1],p-fi[i-1][k]); } }for (k=0;k<=kk;++k) add(ans,fi[n][k]); printf("%d\n",ans); }
bzoj2436 NOI嘉年华(!!!)
题目大意:已知有m个活动申请,两个场地,同一时间只能有一个场地有活动,且一个场地内的活动可以随意安排(可以相交),问无限制和某个活动必须选的时候举办活动少的那个场地最多举办多少。
思路:对时间离散,预处理gi[i][j]表示i~j这段时间的活动数,fp[i][j],fs[i][j]分别表示0~i和i~n时间内一个场地举办j个活动,另一个最多举办多少,转移的时候枚举最后一段一起给某个场地的时间更新。利用这些数组求出fi[i][j]表示i~j这一段必须选,活动较少的那个场地最多能举办多少活动,fi[i][j]=max{min(x+y+gi[i][j],fp[i-1][x]+fs[j+1][y])},x、y表示0~i-1和j+1~n的一个场地活动数,最后答案应尽量平均,所以最优解的y应该随x单调,复杂度O(n^3)。最后统计某一个必须选的时候n^2枚举所有包含它的区间来计算答案。
注意:最后统计答案一定要枚举所有区间,因为可能某个活动被活动x的区间卡在边界上,导致不能统计这个活动。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 405 using namespace std; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} struct use{int s,t;}ai[N]; int ci[N],cz=0,gi[N][N],fi[N][N],fp[N][N],fs[N][N]; void add(int &x,int y){if (y>x) x=y;} int calc(int s,int t,int x,int y){return min(x+y+gi[s][t],fp[s-1][x]+fs[t+1][y]);} int main(){ int n,m,i,j,k,s,t,ans;m=in(); for (i=1;i<=m;++i){ s=in();t=s+in(); ai[i]=(use){s,t}; ci[++cz]=s;ci[++cz]=t; }sort(ci+1,ci+cz+1); n=cz=unique(ci+1,ci+cz+1)-ci-1; memset(gi,0,sizeof(gi)); memset(fi,0,sizeof(fi)); memset(fp,-60,sizeof(fp)); memset(fs,-60,sizeof(fs)); for (i=1;i<=m;++i){ s=upper_bound(ci+1,ci+cz+1,ai[i].s)-ci-1; t=upper_bound(ci+1,ci+cz+1,ai[i].t)-ci-2; for (j=1;j<=s;++j) for (k=t;k<=n;++k) ++gi[j][k]; }fp[0][0]=fs[n+1][0]=0; for (i=1;i<=n;++i) for (j=0;j<i;++j){ for (k=m-gi[j+1][i];k>=0;--k) add(fp[i][k+gi[j+1][i]],fp[j][k]); for (k=m;k>=0;--k) add(fp[i][k],fp[j][k]+gi[j+1][i]); } for (i=n;i;--i) for (j=n+1;j>i;--j){ for (k=m-gi[i][j-1];k>=0;--k) add(fs[i][k+gi[i][j-1]],fs[j][k]); for (k=m;k>=0;--k) add(fs[i][k],fs[j][k]+gi[i][j-1]); } for (ans=0,i=1;i<=m;++i) add(ans,min(i,fp[n][i])); printf("%d\n",ans); for (i=1;i<=n;++i) for (s=1;s+i-1<=n;++s){ t=s+i-1;k=m; for (j=0;j<=m;++j){ while(k&&calc(s,t,j,k)<=calc(s,t,j,k-1)) --k; add(fi[s][t],calc(s,t,j,k)); } } for (i=1;i<=m;++i){ s=upper_bound(ci+1,ci+cz+1,ai[i].s)-ci-1; t=upper_bound(ci+1,ci+cz+1,ai[i].t)-ci-2; ans=0; for (j=1;j<=s;++j) for (k=t;k<=n;++k) ans=max(ans,fi[j][k]); printf("%d\n",ans); } }
bzoj1566 管道取珠(!!)
题目大意:给出两个序列有A/B,每次从一个的右边取出一个,最后形成k种序列,能形成每种的操作方案有ai个,求sigma(i=1~k)ai^2。
思路:因为是平方,相当于取两遍且取出的序列一样的方案数。fi[i][j][a][b]表示第一个人上面取到i、下面到j,第二个人上面到a、下面到b的方案数,b=i+j-a,可以约掉,然后转移。
注意:1)每种转移都要判断序列中的是否一样;
2)从左到右和从右到左取都是一样的。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 505 #define p 1024523 using namespace std; int fi[N][N][N]; char s1[N],s2[N]; void add(int &x,int y){x+=y;if (x>=p) x-=p;} int main(){ int i,j,a,b,n,m; scanf("%d%d%s%s",&n,&m,s1+1,s2+1); memset(fi,0,sizeof(fi)); fi[0][0][0]=1; for (i=0;i<=n;++i) for (j=0;j<=m;++j) for (a=0;a<=n;++a){ if (!fi[i][j][a]) continue; b=i+j-a; if (b<0) continue; if (i<n&&a<n&&s1[n-i]==s1[n-a]) add(fi[i+1][j][a+1],fi[i][j][a]); if (i<n&&b<m&&s1[n-i]==s2[m-b]) add(fi[i+1][j][a],fi[i][j][a]); if (j<m&&a<n&&s2[m-j]==s1[n-a]) add(fi[i][j+1][a+1],fi[i][j][a]); if (j<m&&b<m&&s2[m-j]==s2[m-b]) add(fi[i][j+1][a],fi[i][j][a]); } printf("%d\n",fi[n][m][n]); }
bzoj1564 二叉查找树(!!)
题目大意:给出一棵treap,每个点有数据值d、权值v、访问频度p,可以改变权值为任意实数代价为kk,问修改代价+sigma dep*p最小值。
思路:按d排序之后,就是中序遍历了。把v离散,fi[l][r][k]表示l~r这一段,子树中权值>=k的最小代价。枚举根i,如果vi>=w,就可以不用改根,fi[l][r][k]=min(fi[l][i-1][vi]+fi[i+1][r][vi]+sm[l~i-1]+sm[i+1~r]);还可以一定改根,fi[l][r][k]=min(fi[l][i-1][k]+fi[i+1][r][k]+kk+sm[l~i-1]+sm[i+1~r])。fi[l][r][k]还要和fi[l][r][k+1]取min。
注意:因为可以改成实数,所以k不用+1;但是vi要+1,因为后面的数一定要修改成>vi的数。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 75 #define M 215 #define LL long long #define inf 1000000000000000000LL using namespace std; struct use{ int d,v;LL p; bool operator<(const use&x)const{return d<x.d;} }ai[N]; LL fi[N][N][M],kk,sm[N]={0}; int ci[N],cz=0; bool vi[N][N][M]; LL gets(int l,int r){return sm[r]-sm[l-1];} void add(LL &x,LL y){if (y<x) x=y;} LL dp(int l,int r,int k){ if (k>cz) return inf; if (vi[l][r][k]) return fi[l][r][k]; vi[l][r][k]=true; int i;LL cc;fi[l][r][k]=inf; add(fi[l][r][k],dp(l,r,k+1)); for (i=l;i<=r;++i){ cc=kk; if (i>l) cc+=dp(l,i-1,k)+gets(l,i-1); if (i<r) cc+=dp(i+1,r,k)+gets(i+1,r); add(fi[l][r][k],cc); if (ai[i].v>=k){ cc=0LL; if (i>l) cc+=dp(l,i-1,ai[i].v+1)+gets(l,i-1); if (i<r) cc+=dp(i+1,r,ai[i].v+1)+gets(i+1,r); add(fi[l][r][k],cc); } }return fi[l][r][k]; } int main(){ int n,i;scanf("%d%I64d",&n,&kk); for (i=1;i<=n;++i) scanf("%d",&ai[i].d); for (i=1;i<=n;++i){ scanf("%d",&ai[i].v); ci[++cz]=ai[i].v; }for (i=1;i<=n;++i) scanf("%I64d",&ai[i].p); sort(ai+1,ai+n+1); sort(ci+1,ci+cz+1); cz=unique(ci+1,ci+cz+1)-ci-1; for (i=1;i<=n;++i){ sm[i]=sm[i-1]+ai[i].p; ai[i].v=upper_bound(ci+1,ci+cz+1,ai[i].v)-ci-1+cz; }cz*=3; memset(vi,false,sizeof(vi)); printf("%I64d\n",dp(1,n,1)+sm[n]); }
省队集训R2 day7 T3(!!!)
题目大意:给出一个1~m的长度为n的数字串,重复t之后得到一个长度为n*t的数字串,求最长上升子序列的长度。
思路:fi[i]表示结尾的数字<=i的最长上升子序列的长度,t最多到m是有用的,所以暴力枚举循环次数min(m,t),然后从前向后枚举数ai[i],更新ai[i]~m的fi,如果不能更新就break这个ai[i]~m的循环,因为每个fi[i]最多被n个数更新,所以复杂度是O(nm)的。
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define N 100005 using namespace std; int in(){ char ch=getchar();int x=0; while(ch<'0'||ch>'9') ch=getchar(); while(ch>='0'&&ch<='9'){ x=x*10+ch-'0';ch=getchar(); }return x;} int ai[N],fi[N]={0}; int main(){ freopen("array.in","r",stdin); freopen("array.out","w",stdout); int n,m,t,i,j; n=in();m=in();t=in(); for (i=1;i<=n;++i) ai[i]=in(); t=min(m,t); while(t--) for (i=1;i<=n;++i) for (j=ai[i];j<=m;++j){ if (fi[ai[i]-1]+1>fi[j]) fi[j]=fi[ai[i]-1]+1; else break; } printf("%d\n",fi[m]); }