文章目录

  • 前言
    • 记忆化搜索
    • 动态规划的基本解题思路
  • 背包问题
    • 01背包
      • (待处理)P1489 猫狗大战
      • 01背包的空间优化问题
      • P1048 采药
      • P1510 精卫填海
      • P1566 加等式
      • P1504 积木城堡
    • 完全背包
      • P1474 货币系统
      • P2904 River Crossing
      • P2725 Stamps
    • 分组背包
      • P2409 Y的积木
      • P2066 机器分配
    • 二维费用背包
      • P1759 通天之潜水
      • P1586 四方定理
    • 依赖背包
      • P1064 金明的预算方案
    • 混合背包
      • P2623 物品选取
  • 坐标DP
    • P1434 滑雪
    • P1004 方格取数
    • Codevs 2853 方格游戏
    • P1508 likecloud
    • P2049 魔术棋子
  • 线性DP
    • P1057 传球游戏
    • P1754 球迷购票问题
    • P1095 守望者的逃离
    • P3399 丝绸之路
    • P1387 最大正方形
    • P1681 最大正方形2
    • P2004 领地选择
    • P2072 宗教问题
    • P1564 膜拜
    • (待处理)P2896 一起吃饭
    • (待处理)P1970 花匠
    • (待处理)P1133 教主的花园
    • (待处理)P1233 木棍加工
  • (待处理)区间DP
    • P2426 删数
    • P2858 Treats for the cows
    • P1435 回文子串
    • P4302 字符串折叠
    • P2466 Sue的小球
    • P1220 关路灯
    • P2470 压缩
  • 单调队列
    • P1886 滑动窗口
    • P1440 求m区间内的最小值
    • P2627 修剪草坪
    • (待处理)P3957 跳房子
    • (待处理)P2216 理想的正方形
  • 单调栈
    • P2782 友好城市
    • P2866 Bad Hair Day
    • P2947 Look Up
    • P4147 玉蟾宫
    • P3467 PLA-Postering
    • (待处理)P3503 KLO-Blocks
    • (不会做,弃疗)P1565 牛宫
    • (该题有BUG,弃疗)P1823 音乐会的等待
  • 二分答案+DP
    • P2370 yyy2015c01的U盘
    • (待处理)P3957 跳房子
  • 悬线法DP
    • P4147 玉蟾宫
    • P1387 最大正方形
    • P1736 创意吃鱼法
    • P1169 棋盘制作
  • 状态压缩
    • P2879 玉米田
    • P2704 炮兵阵地
    • P1896 互不侵犯
    • P3092 没有找零
    • P2915 Make up Cows
    • P3052 Cows in Skyscraper
    • P1171 售货员的难题
    • P3118 电影移动
    • (待处理)P3694 邦邦的大合唱站队
    • (待处理)P2622 关灯问题
    • (待处理)P2051 中国象棋
    • (待处理)P3959 宝藏
    • (待处理)P2595 多米诺骨牌
    • (待处理)P2453 最短距离
    • (待处理)P2167 Bill的挑战
    • (待处理)POJ 2411 Mondriaan's Dream
  • (待处理)轮廓线DP
    • P2595 多米诺骨牌
    • POJ 2411 Mondriaan's Dream
  • (待处理)数位DP
    • P2657 windy数
    • P2518 计数
    • P2602 数字计数
    • P4124 手机号码
  • (待处理)博弈论
    • P1199 三国游戏
    • P1488 肥猫的游戏
    • P2197 NIM游戏
    • P1288 取数游戏2
  • (待处理)高维DP
    • P1436 棋盘分割
    • P2489 迷宫冒险
  • (待处理)树形DP
    • 树的重心
      • P2996 拜访奶牛
      • P1352 没有上司的舞会
      • P1364 医院设置
      • P2986 伟大的奶牛聚集
      • P3478 STA-Station
      • P3047 Nearby Cows
      • P1131 时态同步
      • P2014 选课
      • P2016 战略游戏
      • P2276 消防局的设立
      • (待处理)P3177 树上染色
      • (待处理)P4365 秘密袭击
      • (双倍经验)P3780 苹果树
  • (待处理)期望DP
    • P1850 换教室
    • P1291 百事世界杯之旅
    • P3412 仓鼠找sugar2
    • P3400 随机数生成器
    • P2473 奖励关

最近准备把dp完完整整的复习一遍,开博记录

前言

记忆化搜索

记忆化搜索的定义

· 不依赖任何形式的外部变量

· 答案以返回值而非参数形式存在

· 对于相同参数返回值相同

与动态规划的关系

递归实现转移,因此是反向的

如何写记忆化搜索
  • 方法一

把这道题的dp状态和方程写出来

根据他们写出dfs函数

添加记忆化数组

  • 方法二

写出这道题的暴搜程序(最好是dfs)

将这个dfs改成"无需外部变量"的dfs

添加记忆化数组

动态规划的基本解题思路

四个步骤
确定子问题
定义状态
转移方程
统计答案/避免重复求解

具体过程详见下文 P2758 编辑距离

背包问题

01背包

(待处理)P1489 猫狗大战

01背包的空间优化问题

可以空间优化的根本原因:
第i个状态仅能转移到i-1个
即当一层状态更新完毕,就不会影响其余的状态
如果正向枚举
不满足此性质

P1048 采药

记忆化搜索

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=5010,maxm=5010,inf=0x1f1f1f1f;
int n,t,val[maxn],cost[maxn],ans[maxn][maxn];
int dfs(int vleft,int step)
{if(ans[step][vleft]!=-1)return ans[step][vleft];if(step>n)return ans[step][vleft]=0;int nput=-inf,put=-inf;nput=dfs(vleft,step+1);if(cost[step]<=vleft)put=dfs(vleft-cost[step],step+1)+val[step];return ans[step][vleft]=std::max(nput,put);
}
int main()
{memset(ans,-1,sizeof(ans));scanf("%d%d",&t,&n);for(int i=1;i<=n;i++)scanf("%d%d",&cost[i],&val[i]);printf("%d",dfs(t,1));return 0;
}

递推

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=50100,maxm=50100,inf=0x1f1f1f1f;
int n,t,val[maxn],cost[maxn],ans[maxn];
int main()
{scanf("%d%d",&t,&n);for(register int i=1;i<=n;i++)scanf("%d%d",&cost[i],&val[i]);for(register int i=1;i<=n;i++)for(register int j=t;j>=cost[i];j--)ans[j]=std::max(ans[j],ans[j-cost[i]]+val[i]);printf("%d",ans[t]);return 0;
}

P1510 精卫填海

一开始读错了题,以为要刚好填满,就想把体积作为v,把问题转化成可行性背包
事实上该题可以把体积作为w,每当f[j]>W时,统计一下答案

#include<iostream>
#include<cstdio>
int const maxn=10101,maxm=10101,inf=0x1f1f1f1f;
int W,n,V,cv[maxn],w[maxn],f[maxn],ans;
int main()
{ans=-inf;scanf("%d%d%d",&W,&n,&V);for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&cv[i]);for(int i=1;i<=n;i++)for(int j=V;j>=cv[i];j--){f[j]=std::max(f[j],f[j-cv[i]]+w[i]);if(f[j]>=W)ans=std::max(ans,V-j);}if(ans<0){puts("Impossible");return 0;}printf("%d",ans);return 0;
}

P1566 加等式

模型竟然是可行性01背包求解方案数问题,完全没看出来…
题意其实是取某些数使他们的和等于集合内的某个数,每个数只能取一次,问有多少种方案,这样就很明显了
把集合内最大的数作为上限,背包必须填满
跑完背包只要把每个数对应的背包加起来即可
注意!要把自己相等的方案减去

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int t,n,f[1110],a[maxn],ans,mx;
int main()
{scanf("%d",&t);while(t--){memset(f,0,sizeof(f));ans=0;mx=-1;scanf("%d",&n);f[0]=1;for(int i=1;i<=n;i++)scanf("%d",&a[i]),mx=std::max(mx,a[i]);for(int i=1;i<=n;i++)for(int j=mx;j>=a[i];j--)f[j]+=f[j-a[i]];for(int i=1;i<=n;i++)ans+=f[a[i]];printf("%d\n",ans-n);}return 0;
}

P1504 积木城堡

还算有意思的可行性01背包
然而这种水题我竟然没看出来
对于每一堆积木,都求一下可行性,用桶统计一下每个高度的可行性
当某个高度的的可行性达到n种,说明n堆积木都能凑出这个高度,作为一个可行解
从高到低枚举保证答案最优
没有可行解要特判

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1110,maxm=100110,inf=0x1f1f1f1f;
int f[maxm],cv[maxn];
int n,sum,mn=inf,cnt,ton[maxm];
int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
int main()
{scanf("%d",&n);for(int i=1;i<=n;i++){memset(f,0,sizeof(f));f[0]=1;cnt=0,sum=0;for(int x;;){scanf("%d",&x);if(x==-1)break;cv[++cnt]=x;sum+=x;          }mn=std::min(mn,sum);for(int j=1;j<=cnt;j++)for(int k=sum;k>=cv[j];k--)f[k]=std::max(f[k],f[k-cv[j]]);for(int k=1;k<=sum;k++)ton[k]+=f[k];}for(int k=mn;k>=0;k--)if(ton[k]==n){printf("%d",k);return 0;}printf("0");return 0;
}

完全背包

P1474 货币系统

完全背包统计可行性方案数问题
其实不是特别明白…
感性理解一下就是,选择某个面值,就能获得组成当前面值的方案数
因为选择某个数,方案数是不会改变的

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef long long ll;
int const maxn=10110,maxm=10110,inf=0x1f1f1f1f;
int n,T;
ll f[maxn],cv[maxn];
int main()
{scanf("%d%d",&n,&T);for(int i=1;i<=n;i++)scanf("%lld",cv+i);f[0]=1;for(int i=1;i<=n;i++)for(int j=cv[i];j<=T;j++)f[j]+=f[j-cv[i]];printf("%lld",f[T]);return 0;
}

P2904 River Crossing

比较显然的背包问题
把奶牛个数看成容积,耗时看做价值
问题转化为了可行性最小背包问题
由于同一奶牛个数可以重复选,比如可以每次只带一只奶牛所以是完全背包

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=2511,maxm=2511;
int f[maxn],pre,n,m;
int main()
{memset(f,0x1f,sizeof(f));scanf("%d%d",&n,&m);f[0]=-m,pre=m;for(int w,i=1;i<=n;i++){scanf("%d",&w),pre+=w;for(int j=i;j<=n;j++)f[j]=std::min(f[j],f[j-i]+pre+m);}printf("%d",f[n]);return 0;
}

P2725 Stamps

一开始没看到从1开始…
而且价值跟容积完全搞反了…
可行性完全背包
f[i]表示装到价值为i最少需要多少邮票
当f[j-w]>=sum时不能转移

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=54,maxm=2001000,inf=0x1f1f1f1f;
int f[maxm];
int n,sum;
int main()
{memset(f,0x1f,sizeof(f));f[0]=0;scanf("%d%d",&sum,&n);for(int w,i=1;i<=n;i++){scanf("%d",&w);for(int j=w;j<=maxm;j++){if(f[j-w]>=sum)continue;f[j]=std::min(f[j],f[j-w]+1);}}for(int i=1;i<=maxm;i++)if(f[i]==inf){printf("%d",i-1);return 0;}
}

分组背包

P2409 Y的积木

分组背包的可行性方案数问题
f[i][j]表示前i种的和为j的方案数
对于每一种,都跑一遍01背包
因为每个种都从上一种转移,所以每一种一定只能选一个

#include<iostream>
#include<cstdio>
#include<cmath>
int const maxn=10011,maxm=111,inf=0x1f1f1f1f;
int f[maxm][maxn],n,K,cv[maxm][maxm],maxx[maxm],minn[maxm],mx,mn;
int main()
{scanf("%d%d",&n,&K);for(int i=1;i<=n;i++){maxx[i]=-1;scanf("%d",&cv[i][0]);for(int k=1;k<=cv[i][0];k++){scanf("%d",&cv[i][k]);maxx[i]=std::max(maxx[i],cv[i][k]);}mx+=maxx[i];}f[0][0]=1;for(int i=1;i<=n;i++)for(int j=1;j<=cv[i][0];j++)for(int k=mx;k>=cv[i][j];k--)f[i][k]+=f[i-1][k-cv[i][j]];  for(int k=1;k<=mx&&K;k++){while(f[n][k]&&K){f[n][k]--,K--;printf("%d ",k);}}return 0;
}

P2066 机器分配

没想到区间dp也能用分组背包水…
最优性分组背包+路径记录
分为n组,每一组的物品是选1~m个物品,价值是收益
注意!!!分组背包的滚动仅限于最优解背包,不适用方案数背包!
价值应该是w[i][k]

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int f[maxm],w[maxn][maxm],path[maxn][maxm];
int n,V;
int main()
{scanf("%d%d",&n,&V);for(int i=1;i<=n;i++)for(int j=1;j<=V;j++)scanf("%d",&w[i][j]);for(int i=1;i<=n;i++)for(int j=V;j>=1;j--)for(int k=1;k<=j;k++){int flag=0;int lst=j-k;if(f[j]<f[lst]+w[i][k]){f[j]=f[lst]+w[i][k];path[i][j]=k;for(int l=1;l<i;l++)path[l][j]=path[l][lst];flag=true;}if(flag)continue;if(f[j]==f[lst]+w[i][k]){for(int l=1;l<i;l++){if(path[l][lst]==path[l][j])continue;if(path[l][lst]<path[l][j]){flag=true;break;}}}if(flag){for(int l=1;l<i;l++)path[l][j]=path[l][lst];path[i][j]=k;}}printf("%d\n",f[V]);for(int i=1;i<=n;i++)printf("%d %d\n",i,path[i][V]);return 0;
}

二维费用背包

相当于有两个限制条件的背包
基本转移如下

for(int i=1;i<=n;i++)for(int j=下限1;j<=限制1;j++)for(int k=下限2;k<=限制2;k++)f[j][k]=std::max(f[j][k],f[j-cv1[i]][k-cv2[i]);

P1759 通天之潜水

裸的二维费用背包+输出路径

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int f[maxm][maxm],cv1[maxm],cv2[maxn],w[maxn],path[maxm][maxm][maxn];
int n,V1,V2;
int main()
{scanf("%d%d%d",&V1,&V2,&n);for(int i=1;i<=n;i++)scanf("%d%d%d",&cv1[i],&cv2[i],&w[i]);for(int i=1;i<=n;i++)for(int j=V1;j>=cv1[i];j--)for(int k=V2;k>=cv2[i];k--){int lst1=j-cv1[i],lst2=k-cv2[i];if(f[j][k]<f[lst1][lst2]+w[i]){f[j][k]=f[lst1][lst2]+w[i];path[j][k][0]=path[lst1][lst2][0]+1;path[j][k][path[j][k][0]]=i;for(int l=1;l<=path[lst1][lst2][0];l++)path[j][k][l]=path[lst1][lst2][l];}}printf("%d\n",f[V1][V2]);for(int i=1;i<=path[V1][V2][0];i++)printf("%d ",path[V1][V2][i]);return 0;
}

P1586 四方定理

二维费用方案数背包
一开始试图用Y的积木的思路写,给他分成四组
结果这道题要求不超过四个,所以不能这么做
虽然这两者很像,但还是略有不同
分组背包的状态表示的是考虑完i组限制j能得到的答案
二维费用背包表示考虑完限制i后限制j能得得到的答案
(根据这道题的特殊性,我们需要把每个限制i得到的答案加起来)

#include<cstdio>
#include<iostream>
using namespace std;
int f[5][32770],n,t;
int main()
{f[0][0]=1,n=32768;for(int i=1;i*i<=n;i++)for(int j=i*i;j<=n;j++)for(int k=1;k<=4;k++)f[k][j]+=f[k-1][j-i*i];scanf("%d",&t);while(t--){scanf("%d",&n);int ans=0;for(int k=1;k<=4;k++)ans+=f[k][n];printf("%d\n",ans);}return 0;
}

依赖背包

P1064 金明的预算方案

混合背包

把上述所有背包给合起来即可

P2623 物品选取

分组背包+多重背包+完全背包

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=2112,inf=0x1f1f1f1f;
int n,V,f[maxn];
int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
void Pro(int a,int b)
{for(int j=V;j>=0;j--)for(int v=0;v<=j;v++){int w=a*v*v-b*v;f[j]=std::max(f[j],f[j-v]+w);}
}
void Pzo(int v,int w)
{for(int j=V;j>=v;j--)f[j]=std::max(f[j],f[j-v]+w);
}
void Pcp(int v,int w)
{for(int j=v;j<=V;j++)f[j]=std::max(f[j],f[j-v]+w);
}
void Pmu(int v,int w,int nm)
{if(v*nm>=V){Pcp(v,w);return;}int k=1;while(k<=nm){Pzo(v*k,w*k);nm-=k;k*=2;}Pzo(v*nm,w*nm);
}
int main()
{scanf("%d%d",&n,&V);for(int op,w,cv,num,i=1;i<=n;i++){scanf("%d%d%d",&op,&w,&cv);if(op==1)Pro(w,cv);else if(op==2){scanf("%d",&num);Pmu(cv,w,num);}elsePcp(cv,w);}printf("%d",f[V]);return 0;
}

坐标DP

P1434 滑雪

  • 记搜写法
    简单好用
 #include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};
int map[maxn][maxn],f[maxn][maxn],ans,n,m;
int cmp(int x,int y)
{return map[x]<map[y];
}
int dfs(int x,int y)
{if(x>n||x<1||y>m||y<1)return 0;if(f[x][y])return f[x][y];int nans=1;for(int p=0;p<4;p++){int nx=x+dx[p],ny=y+dy[p];if(map[x][y]>map[nx][ny])nans=std::max(nans,dfs(nx,ny)+1);}return f[x][y]=nans;
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&map[i][j]);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)ans=std::max(ans,dfs(i,j));printf("%d",ans);return 0;
}
  • 递推写法
    转移与记搜刚好相反,从低向高转移,只要排一下序就能保证某一状态仅对临近状态有贡献,从而保证无后效性
    再考虑状态,最朴素的思路是二维数组,然而不太好排序
    我们考虑把二维转成一维
    把每一行的开头接在上一行的末尾
    这样就会得到一个这样的表格
i 1 2 3 4 5 6 7 8 9 10 11 12
ai 1 2 3 4 5 16 17 18 19 6 15 24

我们发现一个数x的上下左右有这样的关系,于是就可以转移了

x-m-1 x-m x-m+1
x-1 x x+1
x+m-1 x+m x+m+1
注意边界!
当且仅当x-m≤0时,x位于最上一行;
当且仅当x+m>n*m时,x位于最下一行;
当且仅当x mod m=0时,x位于最右一行;
当且仅当(X-1) mod m =0时,x位于最左一行。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=10110,maxm=10110,inf=0x1f1f1f1f;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};
int map[maxn],a[maxn],ans,f[maxn];
int cmp(int x,int y)
{return map[x]<map[y];
}
int main()
{int n,m;scanf("%d%d",&n,&m);int len=n*m;for(int i=1;i<=len;i++)scanf("%d",&map[i]),a[i]=i;
//  for(int i=1;i<=len;i++)
//      printf("!!!%d %d\n",i,map[i]);std::sort(a+1,a+1+len,cmp);for(int i=1;i<=len;i++){
//      printf("%d\n",a[i]);int x=a[i];f[x]=1;if(x-m>0 && map[x]>map[x-m])f[x]=std::max(f[x],f[x-m]+1);if((x-1)%m && map[x]>map[x-1])f[x]=std::max(f[x],f[x-1]+1);if(x+m<=len && map[x]>map[x+m])f[x]=std::max(f[x],f[x+m]+1);if(x%m && map[x]>map[x+1])f[x]=std::max(f[x],f[x+1]+1);ans=std::max(ans,f[x]);}printf("%d",ans);return 0;
}

P1004 方格取数

  • O(n^4)做法
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=51,maxm=51,inf=0x1f1f1f1f;
int const dx[4]={0,0,-1,-1};
int const dy[4]={-1,-1,0,0};
int const dxf[4]={-1,0,-1,0};
int const dyf[4]={0,-1,0,-1};
int n,m,map[maxn][maxn],f[maxn][maxn][maxn][maxn];
int main()
{scanf("%d",&n);for(int x,y,z;;){scanf("%d%d%d",&x,&y,&z);if(!x&&!y&&!z)break;map[x][y]=z;}for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)for(int k=1;k<=n;k++){int l=i+j-k;if(l<=0)continue;for(int p=0;p<4;p++)f[i][j][k][l]=std::max(f[i][j][k][l],f[i+dx[p]][j+dy[p]][k+dxf[p]][l+dyf[p]]);f[i][j][k][l]+=(map[i][j]+map[k][l]);if(i==k&&j==l)f[i][j][k][l]-=map[i][j];}printf("%d",f[n][n][n][n]);return 0;
}

Codevs 2853 方格游戏

空间控制在n^3就已经够了

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef unsigned long long ull;
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int const dx[4]={0,0,-1,-1};
int const dy[4]={-1,-1,0,0};
int const dxf[4]={-1,0,-1,0};
int const dyf[4]={0,-1,0,-1};
int map[maxn][maxn],f[321][maxn][maxn];
int abs(int x)
{if(x<0)return -x;return x;
}
int main()
{int n;scanf("%d",&n);for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)scanf("%d",&map[i][j]);for(int k=1;k<(n<<1);k++)for(int i=1;i<=std::min(n,k);i++)for(int j=1;j<=std::min(n,k);j++){for(int p=0;p<4;p++){if(i+dx[p]<1 || !(k-i+dy[p]) || j+dxf[p]<1 || !(k-i+dyf[p]))continue;f[k][i][j]=std::max(f[k][i][j],f[k-1][i+dx[p]][j+dxf[p]]);}f[k][i][j]+=abs(map[i][k-i+1]-map[j][k-j+1]);}printf("%d\n",f[2*n-1][n][n]);return 0;
}

P1508 likecloud

其实这题根本就是那道时代的眼泪,那道在ioi上横空出世的一道神题
但还是写出锅来了…
边界可以直接memset
必须从第一行开始搜,因为需要给他们赋上点权
从上往下搜比较暴力,从下往上搜写崩了,等等再调吧…

  • 从上往下
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn],ans,w[maxn][maxn];
int max(int x,int y,int z)
{return std::max(std::max(x,y),z);
}
int main()
{memset(w,-0x1f,sizeof(w));scanf("%d%d",&n,&m);int x=(m/2+1);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&w[i][j]);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)f[i][j]+=max(f[i-1][j-1],f[i-1][j],f[i-1][j+1])+w[i][j];printf("%d",max(f[n][x],f[n][x+1],f[n][x-1]));return 0;
}

P2049 魔术棋子

这道题一看似乎是搜索,然而2^n肯定是过不了的
观察数据,k<=100,应该想到是关于剩余容量可能性的dp
f[i][j][k]表示i,j处是否能得到k这个数
最朴素的转移是枚举所有状态,枚举上一次的所有可能性看看能不能得到当前状态,复杂度O(nmk^2)
不过我们发现,枚举当前状态效率很低,因为有很多废状态根本不可能由上一个转移而来吗
所以我们直接枚举上一次的所有状态即可,复杂度O(nmk)稳如老狗

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int n,m,p,w[maxn][maxn],f[maxn][maxn][maxn],ans,anss[maxn];
int main()
{scanf("%d%d%d",&n,&m,&p);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&w[i][j]);f[1][1][w[1][1]%p]=1;for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)for(int k=0;k<p;k++){if(f[i][j][k*(w[i][j]%p)%p])continue;f[i][j][k*(w[i][j]%p)%p]=f[i-1][j][k]|f[i][j-1][k];}for(int k=0;k<p;k++){ans+=f[n][m][k];if(f[n][m][k])anss[ans]=k;}printf("%d\n",ans);for(int i=1;i<=ans;i++)printf("%d ",anss[i]);return 0;
}

线性DP

P1057 传球游戏

感觉这题递推很神,于是就写了比较友善的记搜
顺便剪了个并无卵用的枝
在条件允许的时候可以两次两次跳,特殊地,有两种可能跳回原地,应注意

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn];
int dfs(int u,int step)
{u+=u<=0?n:0;u+=u>n?-n:0;if(f[u][step])return f[u][step];if(step==m)return f[u][step]=u==1;return f[u][step]=step==m-1?dfs(u+1,step+1)+dfs(u-1,step+1):dfs(u+2,step+2)+dfs(u-2,step+2)+dfs(u,step+2)*2;
}
int main()
{scanf("%d%d",&n,&m);printf("%d",dfs(1,0));return 0;
}

递推写法
一直以为两层循环1-n在第一层无法转移
结果发现如果1-m在第一层就没问题了
第二次移动一定是由第一次移动更新而来
记搜是枚举点判断次数
递推应该反过来

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn];
int dfs(int u,int step)
{u+=u<=0?n:0;u+=u>n?-n:0;if(f[u][step])return f[u][step];if(step==m)return f[u][step]=u==1;return f[u][step]=step==m-1?dfs(u+1,step+1)+dfs(u-1,step+1):dfs(u+2,step+2)+dfs(u-2,step+2)+dfs(u,step+2)*2;
}
int main()
{scanf("%d%d",&n,&m);f[1][0]=1;for(int i=1;i<=m;i++){f[1][i]=f[n][i-1]+f[2][i-1];for(int j=2;j<n;j++)f[j][i]=f[j-1][i-1]+f[j+1][i-1];f[n][i]=f[1][i-1]+f[n-1][i-1];}printf("%d",f[1][m]);return 0;
}

P1754 球迷购票问题

很明显,50一定在100前
问题转化为括号匹配,然后直接套卡特兰数
f[i][j]表示已经拿了i张50,j张100
转移考虑对于每一种(i,j)都能从上一张的两种选择更新

for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)f[i][j]=f[i-1][j]+f[i][j-1];

只有50的状态为初始状态

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
typedef long long ll;
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n;
ll f[maxn][maxn];
int main()
{scanf("%d",&n);for(int i=1;i<=n;i++)f[i][0]=1;for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)f[i][j]=f[i-1][j]+f[i][j-1];printf("%lld",f[n][n]);return 0;
}

P1095 守望者的逃离

time是c的关键字!!!
事实证明能用贪心做的dp尽量分着顺序写,每一部分单独考虑

#include<iostream>
#include<cstdio>
int const maxn=300110,maxm=111,inf=0x1f1f1f1f;
int m,s,tim,f[maxn];
int main()
{scanf("%d%d%d",&m,&s,&tim);for(int i=1;i<=tim;i++){if(m>=10)f[i]=f[i-1]+60,m-=10;elsef[i]=f[i-1],m+=4;}//优先处理闪烁for(int i=1;i<=tim;i++){f[i]=std::max(f[i-1]+17,f[i]);if(f[i]>=s){puts("Yes");printf("%d",i);return 0;}}puts("No");printf("%d",f[tim]);return 0;
}

P3399 丝绸之路

事实证明,状态对于动态规划重要性远大于其他!
才不会说我搞了个走到第i个城市休息了j天的状态然后不会初始化就一直锅着
f[i][j]表示第i天在第j个城市处

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1011,maxm=111,inf=0x1f1f1f1f;
int n,m,np[maxn],w[maxn],f[maxn][maxn];
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d",&np[i]);for(int i=1;i<=m;i++)scanf("%d",&w[i]);memset(f,0x1f,sizeof(f));for(int i=0;i<=m;i++)f[i][0]=0;for(int i=1;i<=m;i++)for(int j=1;j<=n;j++)f[i][j]=std::min(f[i-1][j-1]+w[i]*np[j],f[i-1][j]);printf("%d",f[m][n]);return 0;
}

P1387 最大正方形

水题,不过考虑到用二维前缀和直接水岂不是很low做题的初心,还是写了些递推,然而根本不会
因为最大的正方形一定不是以0结尾
所以我们可以用f[i][j]表示右下角为(i,j)的最大正方形
右下角如果为零,显然就不符合状态 if(!a[i][j]) f[i][j]=0;
对于正方形,我们肯定是想考虑他的内部填充,但对于每一次扩大出来的点,还是要处理一下,作为左右边界
因此转移就可以表示为
f[i][j]=min(f[i-1][j-1], 从当前位置向上延伸连续的1的个数, 当前位置左侧延伸连续1的个数)
简化一下会发现后两条就是对正方形边缘的处理
等价于f[i][j]=min(f[i-1][j],f[i][j-1],f[i-1][j-1])+1;


以下为二周目后的深刻理解

#include<cstdio>
#include<iostream>
int const maxn=111;
int f[maxn][maxn],n,m,ans;
int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)for(int a,j=1;j<=m;j++)scanf("%d",&a),f[i][j]=a?min(f[i-1][j],f[i][j-1],f[i-1][j-1])+a:0,ans=std::max(ans,f[i][j]);printf("%d",ans);return 0;
}

P1681 最大正方形2

这题感觉跟最大正方形1一毛一样啊,就是转移条件变了变
f[i][j]表示右下角在(i,j)长度最大正方形
对于每一个f[i][j],我们都希望去考虑他的子结构
包括

  1. 内部填充
  2. 向左边延伸
  3. 向右边延伸

然后对(i,j)这个点特殊处理一下即可(显然只有(a[i][j] a[i-1][j-1]相等 a[i-1][j] a[i][j-1]相等,两对不等才合法)

转移方程

if(!(a[i][j]^a[i-1][j-1]^a[i-1][j]^a[i][j-1]))f[i][j]=min(f[i-1][j-1],f[i-1][j],f[i][j-1])+1;

最后答案记得+1,原因很显然:没有初始化

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1611,inf=0x1f1f1f1f;
int f[maxn][maxn],a[maxn][maxn],n,m,ans;
int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){if(!(a[i][j]^a[i-1][j-1]^a[i-1][j]^a[i][j-1]))f[i][j]=min(f[i-1][j-1],f[i-1][j],f[i][j-1])+1;ans=std::max(f[i][j],ans);}if(!ans)ans--;//特判,原因显然是边长为1无法构成01相间的正方形printf("%d",ans+1);return 0;
}

P2004 领地选择

二维前缀和的应用
先求出以(1,1)为左上角每个点为右下角的矩形的和
即求二维前缀和
b[i][j]=b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j];
由于正方形的性质
我们只需要枚举右下角的坐标(x,y)
f[i][j]表示以(i,j)为右下角(i-c,j-c)为左上角的正方形的和
转移方程:f[i][j]=b[i][j]-b[i-c][j]-b[i][j-c]+b[i-c][j-c]

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1611,inf=0x1f1f1f1f;
int f[maxn][maxn],a[maxn][maxn],b[maxn][maxn];
int lastx,lasty,n,m,c,ans=-inf;
int main()
{scanf("%d%d%d",&n,&m,&c);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)scanf("%d",&a[i][j]),b[i][j]=a[i][j]+b[i-1][j]+b[i][j-1]-b[i-1][j-1];for(int i=c;i<=n;i++)for(int j=c;j<=m;j++){f[i][j]=b[i][j]-b[i-c][j]-b[i][j-c]+b[i-c][j-c];if(ans<f[i][j]){ans=f[i][j];lastx=i-c,lasty=j-c;}}  printf("%d %d",lastx+1,lasty+1);return 0;
}

P2072 宗教问题

这题线性结构十分明显,每次转移都要遍历寻找上一次的状态
f[i]表示前i个人至少要分成多少个集合
ff[i]表示前i个人的至少有多少点危险度
易知,每一次的转移都要寻找到上一个集合的状态
因此就有两重循环
i : 1~n 遍历所有点
j : i~1 寻找最后一个集合(也就是当前点所在的集合)的开头元素,注意当最后一个集合元素种数超过k就break
转移很简单
因为j是最后一个集合的开头,j-1就是上一个集合的结尾

f[i]=std::min(f[i],f[j-1]+1);
ff[i]=std::min(ff[i],ff[j-1]+cnt);
//其中cnt为当前集合的元素种数

边界问题
而且由于j-1会访问到0,所以0要特殊处理

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1101,maxm=1101,inf=0x1f1f1f1f;
int n,V,f[maxn],ff[maxn],m,k,bel[maxm],cnt,ton[maxn];
int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
int main()
{scanf("%d%d%d",&n,&m,&k);for(int i=1;i<=n;i++)scanf("%d",&bel[i]);memset(f,0x1f,sizeof(f));memset(ff,0x1f,sizeof(ff));f[0]=0,ff[0]=0;for(int i=1;i<=n;i++){cnt=0;memset(ton,false,sizeof(ton));for(int j=i;j>=1;j--){if(!ton[bel[j]])cnt++,ton[bel[j]]=true;if(cnt>k)break;f[i]=std::min(f[i],f[j-1]+1);ff[i]=std::min(ff[i],ff[j-1]+cnt);}}printf("%d\n%d",f[n],ff[n]);return 0;
}

P1564 膜拜

宗教那道题的弱化版
开始直接把板子改了改码了上去,然后WA了0.5h…
后来当发现不满足条件时,不能直接break掉,因为有可能会反向来人导致条件又满足了,比如m=1时,当前有221,下一个是2,然而下4个是2111,这样条件又满足了

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=2511,inf=0x1f1f1f1f;
int f[maxn],ff[maxn],ton[maxn],bel[maxn];
int n,m;
int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
int abs(int x)
{return x>0?x:-x;
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d",&bel[i]);memset(f,0x1f,sizeof(f));f[0]=0;for(int i=1;i<=n;i++){memset(ton,0,sizeof(ton));for(int j=i;j>=1;j--){ton[bel[j]]++;if(abs(ton[1]-ton[2])<=m||!ton[1]||!ton[2])f[i]=std::min(f[i],f[j-1]+1);}}printf("%d",f[n]);return 0;
}

(待处理)P2896 一起吃饭

(待处理)P1970 花匠

(待处理)P1133 教主的花园

(待处理)P1233 木棍加工

(待处理)区间DP

P2426 删数

P2858 Treats for the cows

P1435 回文子串

P4302 字符串折叠

P2466 Sue的小球

P1220 关路灯

P2470 压缩

单调队列

P1886 滑动窗口

当一个人比你小又比你强,那你就打不过他了
当一个人跟你一样强但比你小,那你就打不过他了*2

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
int const maxn=1001100,maxm=210,inf=0x1f1f1f1f;
int n,k,ans[maxn][2],a[maxn];
struct node
{int tim,val;node(int tim=0,int val=0):tim(tim),val(val){}
};
std::deque<node>q;
std::deque<node>p;
int main()
{scanf("%d%d",&n,&k);for(int x,i=1;i<=n;i++){scanf("%d",&x);while(!q.empty()&&q.back().val>=x)q.pop_back();while(!p.empty()&&p.back().val<=x)p.pop_back();q.push_back(node(i,x));p.push_back(node(i,x));while(i-k>=q.front().tim)q.pop_front();while(i-k>=p.front().tim)p.pop_front();if(i>=k)ans[i][0]=q.front().val,ans[i][1]=p.front().val;}for(int j=0;j<=1;j++){for(int i=k;i<=n;i++)printf("%d ",ans[i][j]);puts("");}return 0;
}

P1440 求m区间内的最小值

强化版的单词背诵

// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
int const maxn=2001000,maxm=210,inf=0x1f1f1f1f;
int n,k,a[2];
struct node
{int tim,val;node(int tim=0,int val=0):tim(tim),val(val){}
};
std::deque<node>q;
int main()
{scanf("%d%d%d",&n,&k,&a[1]);puts("0");for(int i=2;i<=n;i++){scanf("%d",&a[i%2]);while(!q.empty()&&a[(i-1)%2]<=q.back().val)q.pop_back();q.push_back(node(i-1,a[(i-1)%2]));while(i-k>q.front().tim)q.pop_front();printf("%d\n",q.front().val);}return 0;
}

P2627 修剪草坪

看似水题,实则巨坑 起码坑了我2h,且听我慢慢道来
选连续不超过k个的最大值可转化为踢掉最小值,被踢的牛满足两两距离小于k
转移方程f[i]=std::max(f[i],f[j]+w[i]) i-k-1<=j<=i-1
注意边界!!!
对于i,如果选了,则前边k个可以不选,所以可以从[i-k-1,i-1]中转移
另外,本题十分毒瘤,第9个点会莫名其妙的溢出,因此直接define int long long莽上去

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
#define int long long
int const maxn=101100,maxm=210,inf=0x1f1f1f1f;
int n,k;
int sum,f[maxn],mn=inf*1000;
struct node
{int tim,val;node(int tim=0,int val=0):tim(tim),val(val){}
};
std::deque<node>q;
signed main()
{scanf("%lld%lld",&n,&k);for(int i=1;i<=k+1;i++){scanf("%lld",&f[i]),sum+=f[i];while(!q.empty()&&q.back().val>=f[i])  q.pop_back();           q.push_back(node(i,f[i]));}for(int i=k+2;i<=n;i++){scanf("%lld",&f[i]),sum+=f[i];while(!q.empty()&&i-k-1>q.front().tim)  q.pop_front();f[i]+=q.front().val;while(!q.empty()&&q.back().val>=f[i])   q.pop_back();q.push_back(node(i,f[i]));}for(int i=n-k;i<=n;i++)mn=std::min(mn,f[i]);printf("%lld",sum-mn);return 0;
}

(待处理)P3957 跳房子

(待处理)P2216 理想的正方形

单调栈

事实证明,单调栈就是弱化版的单调队列,它无法控制区间长度
好吧我错了,单调栈确实可以用队列来实现,但这种结构仍然称为单调栈

P2782 友好城市

按某一岸排序,在另一岸跑单调栈

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<deque>
int const maxn=201000,maxm=210,inf=0x1f1f1f1f;
int n,ans,nor[maxn],sou[maxn],a[maxn],cnt;
int cmp(int x,int y)
{return nor[x]<nor[y];
}
std::deque<int>q;
int main()
{scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d%d",&nor[i],&sou[i]),a[i]=i;std::sort(a+1,a+1+n,cmp);for(int i=1;i<=n;i++){while(!q.empty()&&sou[a[i]]<=q.back())q.pop_back(),cnt--;q.push_back(sou[a[i]]),cnt++;ans=std::max(ans,cnt);}printf("%d",ans);return 0;
}

P2866 Bad Hair Day

每次多加入一个元素就会对答案产生栈大小的贡献

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
#include<stack>
typedef long long ll;
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int n;
ll ans;
std::stack<int>s;
int main()
{scanf("%d",&n);for(int a,i=1;i<=n;i++){scanf("%d",&a);while(!s.empty()&&a>=s.top())s.pop();ans+=s.size();s.push(a);}printf("%lld",ans);return 0;
}

P2947 Look Up

单调队列完爆单调栈的惨剧

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
#include<stack>
int const maxn=100110,maxm=210,inf=0x1f1f1f1f;
int n,ans[maxn];
struct node
{int t,v;node(int t=0,int v=0):t(t),v(v){}
};
std::deque<node>q;
int main()
{scanf("%d",&n);for(int a,i=1;i<=n;i++){scanf("%d",&a);while(!q.empty()&&a>q.back().v){ans[q.back().t]=i;q.pop_back();}q.push_back(node(i,a));}for(int i=1;i<=n;i++)printf("%d\n",ans[i]);return 0;}

P4147 玉蟾宫

一眼望去,woc这不是悬线法嘛,定睛一看,似乎还真是单调栈的裸题…当然也是悬线法的裸题
维护一个高度单调递增的栈,每次弹栈时候更新,因为满足单增的性质,因此每个后面的元素都可以“使用”前面元素的高度,每次弹栈肯定是由于单增不满足,因此每一个被弹出栈的元素都可以“使用”当前引起弹栈的元素的高度,因此这个元素入栈时宽度为所有被弹元素之和+1

注意:每一行都是独立的,记得及时清栈

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
#include<stack>
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans=-1,bns=-1,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm];
struct node
{int len,ht;node(int len=0,int ht=0):len(len),ht(ht){}
};
std::stack<node>s;
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){char a;std::cin>>a;if(a=='F')up[i][j]=up[i-1][j]+1;int l=0;while(!s.empty()&&up[i][j]<=s.top().ht){l+=s.top().len;ans=std::max(ans,l*s.top().ht);s.pop();}s.push(node(l+1,up[i][j]));}int l=0;while(!s.empty()){l+=s.top().len;ans=std::max(ans,l*s.top().ht);s.pop();}}printf("%d",ans*3);return 0;
}

P3467 PLA-Postering

容易发现宽度不会影响答案
简单的性质:当两栋楼不一样高,一定需要2张海报
维护一个单调递增的队列,只需要搞一下相等的情况就好

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stack>
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int n,ans;
std::stack<int>s;
int main()
{scanf("%d",&n);for(int qaq,a,i=1;i<=n;i++){scanf("%d%d",&qaq,&a);while(!s.empty()&&s.top()>a){ans++;s.pop();}while(!s.empty()&&s.top()==a)s.pop();s.push(a);}while(!s.empty()&&s.top()>-1){ans++;s.pop();}printf("%d",ans);return 0;
}

(待处理)P3503 KLO-Blocks

(不会做,弃疗)P1565 牛宫

不知道为什么枚举右端点然后把左端点进栈的做法不对…
0分代码,改天再调吧…

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
#include<stack>
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans=-1,bns=-1,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm],pre[maxn][maxm];
inline int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
inline int pow(int x)
{return x*x;
}
struct node
{int pos,sum;node(int pos=0,int sum=0):pos(pos),sum(sum){}
};
std::deque<node>q;
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)for(int a,j=1;j<=m;j++)scanf("%d",&a),pre[i][j]=pre[i][j-1]+pre[i-1][j]-pre[i-1][j-1]+a;for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){int hight=j-i+1;for(int k=1;k<=m;k++){int sum=pre[j][k]-pre[i][k];int last=-1,now=0;if(!q.empty())last=q.front().pos;while(!q.empty()&&q.back().sum>sum){now=q.back().pos;q.pop_back();}ans=std::max(ans,hight*(last-now+1));q.push_back(node(k,sum));}if(!q.empty()){int last=q.front().pos,now=0;while(!q.empty()){now=q.back().pos;q.pop_back();}ans=std::max(ans,hight*(last-now+1));}}printf("%d",ans);return 0;
}

(该题有BUG,弃疗)P1823 音乐会的等待

样例中这一段
“1,2,2”
第二个2与1之间并没有大于2的数,然而答案并没有统计上

二分答案+DP

实质上是二分答案,然后用dp检验可行性

P2370 yyy2015c01的U盘

对于大的背包来说,接口越大价值越大,满足单调性
直接二分答案+可行性01背包check是否可行

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1101,inf=0x1f1f1f1f;
int f[maxn],cv[maxn],w[maxn];
int n,p,V,mx=-1,ans;
int check(int mid)
{memset(f,0,sizeof(f));for(int i=1;i<=n;i++){if(cv[i]>mid)continue;for(int j=V;j>=cv[i];j--)f[j]=std::max(f[j],f[j-cv[i]]+w[i]);}for(int j=V;j>=1;j--)if(f[j]>=p)return true;return false;
}
int main()
{scanf("%d%d%d",&n,&p,&V);for(int i=1;i<=n;i++)scanf("%d%d",&cv[i],&w[i]),mx=std::max(mx,cv[i]);int l=1,r=mx;while(l<=r){int mid=(l+r)/2;if(check(mid))ans=mid,r=mid-1;elsel=mid+1;}if(ans){printf("%d",ans);return 0;}printf("No Solution!");return 0;
}

(待处理)P3957 跳房子

悬线法DP

两种写法
一种是存长度,一种是存位置
其实一点差别都没有
以每个(i,j)为悬线的底,让他向左上右延伸
然而,这还不够
因为,l[i][j] 和 r[i][j] 的值都各自取决于 l[i-1][j] 和 r[i-1][j]。(因为为保证成为一个矩形,l[i][j] 不能超过 l[i-1][j],r 同理)
更新的时候顺便搞搞就好了

P4147 玉蟾宫

无法再裸

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm];
char a[maxn][maxm];
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=m;i++)rit[0][i]=m+1;for(int i=1;i<=n;i++){int t=0;for(int j=1;j<=m;j++){std::cin>>a[i][j];if(a[i][j]=='R'){up[i][j]=0,lft[i][j]=0;t=j;continue;}up[i][j]=up[i-1][j]+1;lft[i][j]=std::max(t,lft[i-1][j]);}t=m+1;for(int j=m;j;j--){if(a[i][j]=='R'){rit[i][j]=m+1;t=j;continue;}rit[i][j]=std::min(t,rit[i-1][j]);}for(int j=1;j<=m;j++)ans=std::max(ans,(rit[i][j]-lft[i][j]-1)*up[i][j]);}printf("%d",ans*3);return 0;
}

P1387 最大正方形

相较于玉蟾宫,多了一个限制条件

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
#include<stack>
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],a[maxm];
std::stack<int>s;
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)rit[0][i]=n+1;for(int i=1;i<=n;i++){int pos=0;for(int j=1;j<=m;j++){scanf("%d",&a[j]);if(!a[j]){up[i][j]=lft[i][j]=0;pos=j;continue;}up[i][j]=up[i-1][j]+1;lft[i][j]=std::max(lft[i-1][j],pos);}pos=m+1;for(int j=m;j;j--){if(!a[j]){rit[i][j]=m+1;pos=j;continue;}rit[i][j]=std::min(rit[i-1][j],pos);}for(int j=1;j<=m;j++)ans=std::max(ans,std::min(rit[i][j]-lft[i][j]-1,up[i][j]));}printf("%d",ans);return 0;
}

P1736 创意吃鱼法

正方形的性质+悬线法+不怎么巧妙地转移

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=2511,maxm=2511,inf=0x1f1f1f1f;
int n,m,ans,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm],ff[maxn][maxm],a[maxm];
inline int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){scanf("%d",&a[j]);if(a[j]){up[i][j]=lft[i][j]=0;f[i][j]=min(up[i-1][j],lft[i][j-1],f[i-1][j-1])+1;ans=std::max(ans,f[i][j]);continue;}up[i][j]=up[i-1][j]+1;lft[i][j]=lft[i][j-1]+1;}for(int j=m;j;j--){if(a[j]){rit[i][j]=0;ff[i][j]=min(up[i-1][j],rit[i][j+1],ff[i-1][j+1])+1;ans=std::max(ans,ff[i][j]);continue;}rit[i][j]=rit[i][j+1]+1;}}printf("%d",ans);return 0;
}

P1169 棋盘制作

棋盘的反向同奇偶染色。用于解决交错矩形问题

然后我存长度莫名爆炸
还是位置好用…

#include<iostream>
#include<cstdio>
#include<cstring>
#include<deque>
#include<stack>
int const maxn=2100,maxm=2100,inf=0x1f1f1f1f;
int n,m,ans=-1,bns=-1,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm],a[maxn][maxm];
std::stack<int>s;
inline int min(int x,int y,int z)
{return std::min(std::min(x,y),z);
}
inline int pow(int x)
{return x*x;
}
inline void color(int op)
{memset(up,0,sizeof(up));memset(lft,0,sizeof(lft));memset(rit,0,sizeof(rit));for(int i=1;i<=m;i++)rit[0][i]=m+1;for(int i=1;i<=n;i++){int pos=0;for(int j=1;j<=m;j++){if(a[i][j]!=op){up[i][j]=lft[i][j]=0;pos=j;continue;}up[i][j]=up[i-1][j]+1;lft[i][j]=std::max(pos,lft[i-1][j]);}pos=m+1;for(int j=m;j;j--){if(a[i][j]!=op){rit[i][j]=m+1;pos=j;continue;}rit[i][j]=std::min(pos,rit[i-1][j]);}for(int j=1;j<=m;j++){int x=rit[i][j]-lft[i][j]-1,y=up[i][j];ans=std::max(ans,x*y);bns=std::max(bns,pow(std::min(x,y)));}}
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){scanf("%d",&a[i][j]);if((i+j)&1)a[i][j]=1-a[i][j];}color(1);color(0);printf("%d\n%d",bns,ans);return 0;
}

状态压缩

简单来说状压dp就是把状态压成一个维度,直接进行状态转移
因为转移时要枚举所有状态,因此复杂度是O(2^n)

写状压一定要注意运算符的优先级问题!

  1. ! 取反
  2. 算术运算符 + - * / %
  3. 位运算符 << >>
  4. 判断运算符 > < <= >=
  5. 判断运算符 == !=
  6. 按位运算符 & | ^
  7. 逻辑运算符 && ||
  8. 条件运算符 ?:
  9. 赋值运算符 = +=&=

然而这东西太长了没啥用…

总结点性质

  1. 赋值运算符只有当右边全算完才执行所以优先级是最低的
  2. 逻辑运算符就算要短路也要等一边算完,优先级次低
  3. 算术运算符>位运算符>判断运算符>按位运算符

P2879 玉米田

状压dp的入门题,细节还是挺多的
1种0不种,将每一行作为一个状态,每行都从上一行的合法状态转移而来
合法状态的定义:
1.包含初始状态
2.满足题目的性质(对于本题来说就是1不相邻)

Map[i]表示每行的初始状态
scanf("%d",&a),Map[i]=(Map[i]<<1)+a;
pan[j]表示状态j 在本行是否合法
pan[i]=!(i&i<<1 || i&i>>1);
f[i][j]表示在第i行状态为j时的方案数

转移的时候枚举每行和当前行的状态,当状态包含初始状态且在本行合法时,枚举上一行的状态,看看是否合法

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=15,maxm=5010,inf=0x1f1f1f1f,p=100000000;
int n,m,pan[maxm],Map[maxn],f[maxn][maxm],mx;
inline void change(int x,int pos)
{for(int i=pos;i>=0;i--)printf("%d",x>>i&1);
}
//将一个数转化为二进制数
//用于查错,看看状态是否枚举正确
inline void readin()
{for(int a,i=1;i<=n;i++) for(int j=1;j<=m;j++)scanf("%d",&a),Map[i]=(Map[i]<<1)+a;for(int i=0;i<=mx;i++)pan[i]=!(i&i<<1 || i&i>>1);
}
inline void solve()
{for(int i=1;i<=n;i++)for(int j=0;j<=mx;j++)if((Map[i]&j)==j && pan[j])for(int k=0;k<=mx;k++)if(!(k&j))f[i][j]=(f[i][j]+f[i-1][k])%p;
}
int main()
{scanf("%d%d",&n,&m);mx=(1<<m)-1;f[0][0]=1;readin();solve();int ans=0;for(int i=0;i<=mx;i++)ans=(f[n][i]+ans)%p;printf("%d",ans);return 0;
}

P2704 炮兵阵地

窝好菜啊…这么道水题写了好久…
这题跟玉米田几乎一毛一样
注意一点:列数少,所以应该把每一行给压起来,这样可以枚举所有列的状态!

由这题引出状压dp的时空优化
1.空间优化注意当某一行的状态仅由前几行转移而来时,可以滚动数组优化
2.枚举所有状态无疑是个大工程,所以我们不妨把合法状态给存起来,只枚举合法的
#include<iostream>
#include<cstdio>
int const maxn=111,maxm=1533;
int n,m,mx;
int Map[maxn],Sum[maxm],Pan[maxm],State[maxm],cnt;
int f[2][maxm][maxm];
inline void change(int x)
{for(int i=3;i>=0;i--)printf("%d",x>>i&1);return;
}
inline int get(int x)
{int tot=0;while(x>0){tot+=x&1;x>>=1;}return tot;
}
inline void rin()
{scanf("%d%d",&n,&m);mx=(1<<m)-1;for(int i=1;i<=n;i++){char a[12];scanf("%s",a+1);for(int j=1;j<=m;j++){Map[i]<<=1;if(a[j]=='P')Map[i]++;}}return;
}
inline void pretreatment()
{for(int i=0;i<=mx;i++){Pan[i]=!(i&i>>1||i&i>>2||i&i<<1||i&i<<2);if(Pan[i])State[++cnt]=i;Sum[i]=get(i);}for(int i=1;i<=cnt;i++)if((State[i]&Map[1])==State[i])f[1][0][State[i]]=Sum[State[i]];for(int i=1;i<=cnt;i++)if((State[i]&Map[2])==State[i])for(int j=1;j<=cnt;j++)if((State[j]&Map[1])==State[j]&&!(State[j]&State[i]))f[0][State[j]][State[i]]=std::max(f[0][State[j]][State[i]],f[1][0][State[j]]+Sum[State[i]]);return;
}
inline void solve()
{for(int i=3;i<=n;i++)for(int j=1;j<=cnt;j++)if((State[j]&Map[i])==State[j])for(int k=1;k<=cnt;k++)if((State[k]&Map[i-1])==State[k]&&!(State[k]&State[j]))for(int l=1;l<=cnt;l++)if((State[l]&Map[i-2])==State[l]&&!(State[l]&State[j])&&!(State[l]&State[k]))f[i%2][State[k]][State[j]]=std::max(f[i%2][State[k]][State[j]],f[(i-1)%2][State[l]][State[k]]+Sum[State[j]]);return;
}
inline void write()
{int ans=0;for(int i=1;i<=cnt;i++)for(int j=1;j<=cnt;j++)ans=std::max(f[n%2][State[i]][State[j]],ans);printf("%d",ans);return;
}
int main()
{rin();pretreatment();solve();write();return 0;
}

P1896 互不侵犯

f[i][j][k]表示是第i行状态的编号为j时总共选了k个国王
本来还想搞一发可行性背包
突然想到都枚举所有可能状态了还背个毛线包啊
遂无问津者
另,由以上三题不难看出状压dp中有关个数的部分一般都是找1的个数
若是答案就作为状态的值,若非则作为状态

#include<iostream>
#include<cstdio>
int const maxn=150,maxm=1533;
int n,limit,mx;
int Sta[maxn],cnt,Sum[maxn];
long long f[12][maxm][maxn];
inline void change(int x)
{for(int i=2;i>=0;i--)printf("%d",x>>i&1);return;
}
inline int get(int x)
{int tot=0;while(x){tot+=x&1;x>>=1;}return tot;
}
inline void rin()
{scanf("%d%d",&n,&limit);return;
}
inline void pretreatment()
{mx=(1<<n)-1;for(int i=0;i<=mx;i++)if(!(i&i<<1||i&i>>1)){Sta[++cnt]=i;Sum[cnt]=get(i);f[1][cnt][Sum[cnt]]=1;}return;
}
inline void solve()
{for(int i=2;i<=n;i++)for(int j=1;j<=cnt;j++){int now=Sta[j];for(int k=1;k<=cnt;k++){int last=Sta[k];if(!(now&last||now&last<<1||now&last>>1)){for(int l=0;l<=limit;l++)f[i][j][l+Sum[j]]+=f[i-1][k][l];}}}return;
}
inline void write()
{long long ans=0;for(int i=1;i<=cnt;i++)ans+=f[n][i][limit];printf("%lld",ans);
}
int main()
{rin();pretreatment();solve();write();return 0;
}

P3092 没有找零

这个题刷新了我对状压dp的认识…
详细讲讲
f[i]表示i这个状态时能买到的序号(注意从1开始按顺序买)
怎么找上一个状态呢
比如10110这个状态
这三个1都可能是这次支付的,因此上个状态就是00110 10010 10100 这三个
怎么实现呢
我们可以搞个b数组,存只支付某一张钞票的状态,如001 010 100
我们把他跟状态i与一下,就找到了某一个1
注意f[i]存的是从1可以买到的商品序号
我们发现我们可以存一个前缀和,把上一个状态的前缀和(因为浪费的钱会影响转移)加上这次支付的面额来跟各个前缀和比较得到这次的商品序号
这道题无负数,前缀和满足单调递增,可以直接二分查找
复杂度O(2^(n+1)*logm)

#include<iostream>
#include<cstdio>
int const maxn=100110,maxm=66536;
int n,m,mx,sum;
int f[maxm],pre[maxn],b[maxn],v[15],cv[maxn];
inline void change(int x)
{for(int i=2;i>=0;i--)printf("%d",x>>i&1);printf("    ");return;
}
inline int find(int x)
{int l=0,r=m,ans=0;while(l<=r){int mid=l+r>>1;if(pre[mid]<=x){ans=mid;l=mid+1;}elser=mid-1;}return ans;
}
inline void rin()
{scanf("%d%d",&n,&m);for(int i=1;i<=n;i++)scanf("%d",&v[i]),sum+=v[i];for(int i=1;i<=m;i++)scanf("%d",&cv[i]),pre[i]=pre[i-1]+cv[i];return;
}
inline void pretreatment()
{mx=(1<<n)-1;b[1]=1;for(int i=2;i<=n;i++)b[i]=b[i-1]<<1;
}
inline void solve()
{for(int i=0;i<=mx;i++)for(int j=1;j<=n;j++)if(i&b[j])f[i]=std::max(f[i],find(pre[f[i^b[j]]]+v[j]));
}
inline void write()
{int ans=-1,last=-1;for(int i=0;i<=mx;i++){if(f[i]!=m)continue;int y=0;for(int j=1;j<=n;j++)if(!(b[j]&i))y+=v[j];ans=std::max(ans,y); }printf("%d",ans);return;
}
int main()
{rin();pretreatment();solve();write();return 0;
}

P2915 Make up Cows

跟没有找零的转移思路有点像
都是从枚举状态i中的1,然后删掉某个1从而找到上一状态
f[i][j]表示状态i时最后一个奶牛序号是j的方案数

#include<iostream>
#include<cstdio>
int const maxn=21,maxm=66536,maxe=25100;
long long f[maxm][maxn];
int Pos[maxn],a[maxn];
int n,K,mx;
inline void change(int x)
{for(int i=4;i>=0;i--)printf("%d",x>>i&1);puts("");
}
inline int abs(int x)
{return x<0?-x:x;
}
inline void rin()
{scanf("%d%d",&n,&K);for(int i=1;i<=n;i++)scanf("%d",&a[i]);return;
}
inline void pretreatment()
{mx=(1<<n)-1;Pos[1]=1,f[1][1]=1;for(int i=2;i<=n;i++)Pos[i]=Pos[i-1]<<1,f[Pos[i]][i]=1;
}
inline void solve()
{for(int i=0;i<=mx;i++)for(int j=1;j<=n;j++){if(f[i][j])continue;if(i&Pos[j])for(int k=1;k<=n;k++){int y=i^Pos[j];if(y&Pos[k]&&abs(a[k]-a[j])>K)f[i][j]+=f[y][k];}}
}
inline void write()
{long long ans=0;for(int i=1;i<=n;i++)ans+=f[mx][i];printf("%lld",ans);
}
int main()
{rin();pretreatment();solve();write();return 0;
}

P3052 Cows in Skyscraper

这道题跟上边几道基本一样
f[i]表示i状态需要的最少分组
lftV[i]表示i状态能剩下的最大空间
转移的时候注意一点
f[i]更新的时候lftV[i]一定更新
f[i]不更新的lftV[i]不一定不更新!
我难道会说我就是因为忘了判新开一个组时f[i]不更新的情况而查了1h

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=21,maxm=272144,maxe=501000,inf=0x1f1f1f1f;
int n,V,mx,cv[maxn];
int Pos[maxm];
long long lftV[maxm];int f[maxm];
inline void change(int x)
{for(int i=4;i>=0;i--)printf("%d",x>>i&1);
}
inline void rin()
{scanf("%d%d",&n,&V);for(int i=1;i<=n;i++)scanf("%d",&cv[i]);return;
}
inline void pretreatment()
{mx=(1<<n)-1;Pos[1]=1;for(int i=2;i<=n;i++)Pos[i]=Pos[i-1]<<1;memset(f,0x1f,sizeof(f));for(int i=1;i<=n;i++){lftV[Pos[i]]=V-cv[i];
//      change(Pos[i]);
//      puts("");
//      printf("!%d\n",lftV[Pos[i]]);
//      puts("");f[Pos[i]]=1;}return;
}
inline void solve()
{for(int i=0;i<=mx;i++){if(f[i]!=inf)continue;for(int j=1;j<=n;j++)if(i&Pos[j]){int y=i^Pos[j];
//              change(i);
//              printf("   %d %d\n",f[i],lftV[i]);
//              change(y);
//              printf("   %d %d\n",f[y],lftV[y]);if(lftV[y]-cv[j]>=0){if(f[i]>f[y])f[i]=f[y],lftV[i]=lftV[y]-cv[j];else if(f[i]==f[y])lftV[i]=std::max(lftV[i],lftV[y]-cv[j]);}else{if(f[i]>f[y]+1)f[i]=f[y]+1,lftV[i]=V-cv[j];elseif(f[i]==f[y]+1)lftV[i]=std::max(lftV[i],lftV[y]-cv[j]);
//                  lftV[i]=std::max(lftV[i],V-cv[j]);}
//              printf("!!!%d  %d\n",f[i],lftV[i]);
//              puts("");}}return;
}
inline void write()
{
//  change(mx);
//  puts("");
//  printf("%d %d",f[mx],lftV[mx]);printf("%d",f[mx]); return;
}
int main()
{rin();pretreatment();solve();write();return 0;
}

P1171 售货员的难题

f[i][j]表示i状态时j为结束点的最小值
这题极限卡常,难受

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=23,maxm=1148576,maxe=501000,inf=0x1f1f1f1f;
int n,map[maxn][maxn],mx;
int Pos[maxn];
int f[maxm][maxn];
inline void rin()
{scanf("%d",&n);for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)scanf("%d",&map[i][j]);return;
}
inline void pretreatment()
{memset(f,0x1f,sizeof(f)); mx=(1<<n)-1;Pos[1]=1;for(int i=2;i<=n;i++)Pos[i]=Pos[i-1]<<1;for(int i=2;i<=n;i++)f[1|Pos[i]][i]=map[1][i];
//  f[1][1]=0;return;
}
inline void solve()
{for(int i=2;i<=mx;i++)for(int j=2;j<=n;j++)if(i&Pos[j]){int y=i^Pos[j];for(int k=1;k<=n;k++)if(y&Pos[k])f[i][j]=std::min(f[i][j],f[y][k]+map[k][j]);}return;
}
inline void write()
{int ans=inf;for(int i=1;i<=n;i++)ans=std::min(ans,f[mx][i]+map[i][1]);printf("%d",ans);return;
}
int main()
{rin();pretreatment();solve();write();
}

P3118 电影移动

这题不能填表!!!
像这样

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=23,maxm=1148576,maxe=501000,inf=0x1f1f1f1f;
int n,l,mx,start[maxn][1101],last[maxn],num[maxn];
int Pos[maxn];
int f[maxm];
inline void change(int x)
{for(int i=4;i>=0;i--)printf("%d",x>>i&1);puts("");
}
inline int get(int x)
{int tot=0;while(x){tot+=x&1;x>>=1;}return tot;
}
inline int find(int id,int check)
{int ans=0,l=1,r=num[id]+1;while(l<=r){int mid=l+r>>1;
//      printf("%d %d %d\n",l,r,mid);
//      printf("!!! %d %d\n",start[id][1],last[id]);if(start[id][mid]<=check&&start[id][mid]+last[id]>check){l=mid+1;ans=mid;}elser=mid-1;}return ans;
}
inline void rin()
{scanf("%d%d",&n,&l);for(int i=1;i<=n;i++){scanf("%d%d",&last[i],&num[i]);for(int j=1;j<=num[i];j++)scanf("%d",&start[i][j]);}return;
}
inline void pretreatment()
{mx=(1<<n)-1;Pos[1]=1;for(int i=2;i<=n;i++)Pos[i]=Pos[i-1]<<1;return;
}
inline void solve()
{
//  /*for(int i=0;i<=mx;i++)for(int j=1;j<=n;j++)if(i&Pos[j]){int y=i^Pos[j];int fff=find(j,f[y]);if(!fff)continue;change(i);change(i^Pos[j]);printf("!%d ",fff);printf(" %d ",start[j][fff]+last[j]-f[j]);f[i]=std::max(f[i],start[j][fff]+last[j]);printf("%d\n",f[i]);puts("");}
//          */
//  printf("%d\n",start[2][find(2,65)]);return;
}
inline void write()
{int ans=inf;for(int i=mx;i>=0;i--)if(f[i]>=l)ans=std::min(ans,get(i));if(ans==inf)printf("-1");else printf("%d",ans);return;
}
int main()
{rin();pretreatment();solve();write();return 0;
}

因为对于某个状态来说,其所有子状态的最优并不能保证其最优
而当前状态的合法的下一个状态的最优值一定能使答案最优
(想一想,下一场电影的合法的开始时间中最晚的值一定能使结果最优)

好!从想出填表法到疯狂对拍后发现正解是刷表法共耗时2.5h

然后写完刷表法还是只有29分…
以下是对拍数据,耗时2.5h

5 88
10 3 0 17 25
20 3 2 22 48
30 4 3 27 33 34
40 2 1 15
50 3 0 17 38
ans=3
----------------
2 25
10 3 2 10 20
20 1 0
ans=2
----------------
6 95
10 1 1
20 4 2 11 37 65
30 2 0 5
40 3 3 53 62
50 2 0 27
60 2 3 29
ans=3
----------------
6 99
10 2 2 12
20 2 1 29
30 4 0 17 44 46
40 2 3 10
50 4 0 55 114 126
60 3 1 2 26
ans=3
----------------
6 77
10 3 2 3 7
20 4 3 14 36 57
30 1 0
40 1 2
50 1 0
60 1 2
ans=3

最大的锅是二分写炸了
统计答案要同时满足多个条件
但事实上应该满足start[id][mid]<=check就算合法的二分
只不过不一定是合法的答案而已

写完这道题感觉收获满满

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=23,maxm=1148576,maxe=501000,inf=0x1f1f1f1f;
int n,l,mx,start[maxn][1101],last[maxn],num[maxn];
int Pos[maxn];
int f[maxm];
inline void change(int x)
{for(int i=5;i>=0;i--)printf("%d",x>>i&1);
}
inline int get(int x)
{int tot=0;while(x){tot+=x&1;x>>=1;}return tot;
}
inline int find(int id,int check)
{int ans=0,l=1,r=num[id];while(l<=r){int mid=l+r>>1;
//        printf("~~~%d\n",mid);
//        printf("!!!%d %d\n",start[id][mid]<=check,start[id][mid]+last[id]>check);
//        printf("???%d %d\n",start[id][mid],last[id]);if(start[id][mid]<=check){if(start[id][mid]+last[id]>=check)ans=mid;l=mid+1;}elser=mid-1;}
//    printf("%d\n",ans);return ans;
}
inline void rin()
{scanf("%d%d",&n,&l);for(int i=1;i<=n;i++){scanf("%d%d",&last[i],&num[i]);for(int j=1;j<=num[i];j++)scanf("%d",&start[i][j]);}return;
}
inline void pretreatment()
{mx=(1<<n)-1;Pos[1]=1;for(int i=2;i<=n;i++)Pos[i]=Pos[i-1]<<1;return;
}
inline void solve()
{
//  /*
//  puts("!!!");for(int i=0;i<=mx;i++)for(int j=1;j<=n;j++)if(!(i&Pos[j])){int y=i|Pos[j];int pos=find(j,f[i]);if(!pos)continue;f[y]=std::max(f[y],start[j][pos]+last[j]);
//              printf("~~~%d\n",j);
//              change(i);
//              printf("  %d\n",f[i]);
//              change(y);
//              printf("  %d\n",f[y]);
//              printf("!%d %d\n",pos,f[y]);
//              puts("");}
//          */
//  printf("qwq %d\n",find(2,62));
//  printf("%d %d\n",start[3][3],last[3]);return;
}
inline void write()
{int ans=inf;for(int i=mx;i>=0;i--)if(f[i]>=l)ans=std::min(get(i),ans);if(ans==inf)printf("-1");elseprintf("%d",ans);return;
}
int main()
{
//  freopen("qwq.in","r",stdin);
//  freopen("oup.out","w",stdout);rin();pretreatment();solve();write();return 0;
}

最后附一个数据生成器吧,毕竟也花了不少精力在上面

#include<iostream>
#include<cstdio>
#include<ctime>
#include<cstdlib>
#define rii register int i
#define p 6
#define pp 100
#define ppp 4
using namespace std;
long long seed;
int n,m;
int main()
{freopen("data.in","w",stdout);//文件操作,得到输入文件 seed=time(0);srand(seed);n=rand();//windows下rand()max为32768,为了有一定的强度,我们乘一下 n*=4321;//n,m这里你也可以手动取值 n%=p;n++;m=rand();m*=4321;m%=pp;m++;printf("%d %d\n",n,m);int w=10;for(int i=1;i<=n;i++){printf("%d ",w);w+=10;int ww=(rand()*4321)%ppp+1;printf("%d ",ww);int start=ppp;int qwq=(rand()*4321)%start;for(int j=1;j<=ww;j++){printf("%d ",qwq);qwq+=(rand()*4321)%w+1;}puts("");}return 0;
}

(待处理)P3694 邦邦的大合唱站队

(待处理)P2622 关灯问题

(待处理)P2051 中国象棋

(待处理)P3959 宝藏

(待处理)P2595 多米诺骨牌

(待处理)P2453 最短距离

(待处理)P2167 Bill的挑战

(待处理)POJ 2411 Mondriaan’s Dream

(待处理)轮廓线DP

P2595 多米诺骨牌

POJ 2411 Mondriaan’s Dream

(待处理)数位DP

P2657 windy数

P2518 计数

P2602 数字计数

P4124 手机号码

(待处理)博弈论

P1199 三国游戏

P1488 肥猫的游戏

P2197 NIM游戏

P1288 取数游戏2

(待处理)高维DP

P1436 棋盘分割

P2489 迷宫冒险

(待处理)树形DP

树的重心

树的重心的定义:

树若以某点为根,使得该树最大子树的结点数最小,那么这个点则为该树的重心,一棵树可能有多个重心。

树的重心的性质:

1、树上所有的点到树的重心的距离之和是最短的,如果有多个重心,那么总距离相等。

2、插入或删除一个点,树的重心的位置最多移动一个单位。

3、若添加一条边连接2棵树,那么新树的重心一定在原来两棵树的重心的路径上。

怎么求树的重心:

定义几个数组:f[u]f[u]f[u]表示以u为根的总距离(深度和),size[u]size[u]size[u]表示以u为根的子树的大小

首先我们任意以一个点为根dfs一遍,求出以该点为根的总距离。

转移考虑换根对深度和的影响,对于每个u能达到的点v,有:
f[v]=f[u]+size[1]−size[v]−size[v]f[v]=f[u]+size[1]-size[v]-size[v]f[v]=f[u]+size[1]−size[v]−size[v]

解释一下,−size[v]-size[v]−size[v]是因为v的子树到根的距离减少了1,+size[1]−size[v]+size[1]-size[v]+size[1]−size[v]则是v的子树之外的点到根的距离增加了1

P2996 拜访奶牛

dfs写法
因为要先处理叶子结点
因此可以在回溯的时候处理当前节点

#include<iostream>
#include<cstdio>
#include<queue>
int const maxn=111111,maxm=111,inf=0x1f1f1f1f;
struct E
{int to,next;E(int to=0,int next=0):to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{e[++cnt]=(E){v,head[u]};head[u]=cnt;
}
int n;
int f[maxn][3],ans;
void dfs(int u,int fa)
{f[u][1]=1;for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;dfs(v,u);f[u][0]+=std::max(f[v][0],f[v][1]);f[u][1]+=f[v][0];}
}
int main()
{scanf("%d",&n);for(int u,v,i=1;i<n;i++)scanf("%d%d",&u,&v),add(u,v),add(v,u);dfs(1,0);printf("%d",std::max(f[1][0],f[1][1]));return 0;
}

P1352 没有上司的舞会

怕爆栈?
拓扑序+树形DP教做人
需要先处理子节点,不妨从子节点向父节点建边

#include<iostream>
#include<cstdio>
#include<queue>
int const maxn=6111,maxm=111,inf=0x1f1f1f1f;
int n;
int w[maxn],ind[maxn],fa[maxn];
int f[maxn][3],ans=-inf;
inline int max(int x,int y,int z)
{return std::max(std::max(x,y),z);
}
int main()
{scanf("%d",&n);for(int i=1;i<=n;i++)scanf("%d",w+i);for(int s,fff,i=1;i<n;i++){scanf("%d%d",&s,&fff);fa[s]=fff;ind[fff]++;}std::queue<int>q;for(int i=1;i<=n;i++)if(!ind[i])q.push(i);while(!q.empty()){int u=q.front();int v=fa[u];q.pop();f[u][1]+=w[u];ans=max(ans,f[u][1],f[u][0]);ind[v]--;f[v][0]+=std::max(f[u][1],f[u][0]);f[v][1]+=f[u][0]>0?f[u][0]:0;if(!ind[v])q.push(v);}printf("%d",ans);return 0;
}

P1364 医院设置

树的重心+换根法dp
关键在于每个点可能有很多人
size和f初始化dfs的时候要注意

#include<iostream>
#include<cstdio>
#include<queue>
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
struct E
{int to,next;E(int to=0,int next=0):to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{e[++cnt]=(E){v,head[u]};head[u]=cnt;
}
int n,w[maxn];
int f[maxn],size[maxn];
int ans=inf;
void pre_dfs(int u,int fa,int dep)
{size[u]=w[u];//乘上人数for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;pre_dfs(v,u,dep+1);size[u]+=size[v];}f[1]+=dep*w[u];//乘上人数
}
void c_r_dfs(int u,int fa)
{ans=std::min(ans,f[u]);for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;f[v]=f[u]+-size[v]+size[1]-size[v];c_r_dfs(v,u);}
}
int main()
{scanf("%d",&n);for(int l,r,i=1;i<=n;i++){scanf("%d%d%d",w+i,&l,&r);if(l) add(i,l);if(r) add(i,r);}pre_dfs(1,0,0);c_r_dfs(1,0);printf("%d",ans);return 0;
}

P2986 伟大的奶牛聚集

P3478 STA-Station

嘿嘿嘿我偏不写dfs
Bfs+topsort
喵的真难写,细节贼多,还是dfs好写
另外这题卡精度卡scanf,sb一个

#include<iostream>
#include<cstdio>
#include<queue>
typedef unsigned long long ll;
int const maxn=1001100,maxm=111,inf=0x1f1f1f1f;
struct E
{int to,next;E(int to=0,int next=0):to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{e[++cnt]=(E){v,head[u]};head[u]=cnt;
}
struct node
{int id,fa;node(int id=0,int fa=0):id(id),fa(fa){}
};
int n;
ll ind[maxn];
ll f[maxn];int size[maxn],dep[maxn];
int ans;ll max;
inline int read()
{int re=0;char ch=getchar();while (ch<'0' || ch>'9') ch=getchar();while (ch>='0' && ch<='9'){ re=re*10+ch-'0'; ch=getchar();}return re;
}
int main()
{n=read();for(int u,v,i=1;i<n;i++)u=read(),v=read(),add(u,v),add(v,u),ind[u]++,ind[v]++;std::queue<node>p;dep[0]=-1,p.push(node(1,0));while(!p.empty()){int u=p.front().id,fa=p.front().fa;p.pop();dep[u]=dep[fa]+1;for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;p.push(node(v,u));}}std::queue<int>q;if(ind[1]==1)ind[1]++;for(int i=1;i<=n;i++)if(ind[i]==1)q.push(i);while(!q.empty()){int u=q.front();q.pop();size[u]++;f[1]+=dep[u];for(int i=head[u];i;i=e[i].next){int v=e[i].to;ind[v]--;if(ind[v]<=0)continue;size[v]+=size[u];if(ind[v]==1)q.push(v);}}p.push(node(1,0));while(!p.empty()){int u=p.front().id,fa=p.front().fa;p.pop();if(max<f[u])ans=u,max=f[u];if(max==f[u]&&ans>u)ans=u;for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;f[v]=f[u]-size[v]+size[1]-size[v];p.push(node(v,u));}}printf("%d",ans);return 0;
}

P3047 Nearby Cows

神题
让我刚了2h的题
也是让我搞清了大部分树形dp思路的题
第一遍dfs求出子树内的信息
第二遍搞出非子树信息(一般会对子树内的产生影响,需要容斥)
f[i][j]f[i][j]f[i][j]表示经过最多j步到i点的奶牛数
详见代码

#include<iostream>
#include<cstdio>
#include<queue>
int const maxn=100110,maxm=24,inf=0x1f1f1f1f;
struct E
{int to,next;E(int to=0,int next=0):to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{e[++cnt]=(E){v,head[u]};head[u]=cnt;
}
int n,k,w[maxn];
int f[maxn][maxm],ff[maxn][maxm],fff[maxn][maxm],size[maxn];
void pre_dfs(int u,int fa)
{fff[u][0]=f[u][0]=w[u];for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;pre_dfs(v,u);for(int j=1;j<=k;j++)f[u][j]+=f[v][j-1];}for(int j=1;j<=k;j++)fff[u][j]=f[u][j]+fff[u][j-1];//新开一个数组,要不会对向上回溯产生影响
}
void r_c_dfs(int u,int fa)
{for(int i=head[u];i;i=e[i].next){int v=e[i].to;if(v==fa)continue;ff[v][0]=w[v];ff[v][1]=fff[v][1]+fff[u][0];for(int j=2;j<=k;j++)ff[v][j]=fff[v][j]+ff[u][j-1]-fff[v][j-2];//细节,对于u要用更新后的值,也就是ff数组r_c_dfs(v,u);}
}
int main()
{scanf("%d%d",&n,&k);for(int u,v,i=1;i<n;i++)scanf("%d%d",&u,&v),add(u,v),add(v,u);for(int i=1;i<=n;i++)scanf("%d",w+i);pre_dfs(1,0);for(int j=0;j<=k;j++)ff[1][j]=fff[1][j];//别忘了对1初始化r_c_dfs(1,0);for(int i=1;i<=n;i++)printf("%d\n",ff[i][k]);return 0;
}

P1131 时态同步

从下往上考虑,如果子树中有不同的时间,那么必须搞成一样的
f[i]f[i]f[i]表示i的子树内的最大边权
第一遍dfs处理f[i]
第二遍统计答案

#include<iostream>
#include<cstdio>
#define int long long
int const maxn=500130,maxm=111,inf=0x1f1f1f1f;
inline int abs(int x)
{return x>0?x:-x;
}
struct E
{int to,next,w;E(int to=0,int next=0,int w=0):to(to),next(next),w(w){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v,int w)
{e[++cnt]=(E){v,head[u],w};head[u]=cnt;
}
int n,root;
int f[maxn];
int ans;
int pre_dfs(int u,int fa)
{int max=0;for(int i=head[u];i;i=e[i].next){int v=e[i].to,w=e[i].w;if(v==fa)continue;max=std::max(max,pre_dfs(v,u)+w);}return f[u]=max;
}
void sv_dfs(int u,int fa)
{for(int i=head[u];i;i=e[i].next){int v=e[i].to,w=e[i].w;if(v==fa)continue;ans+=f[u]-f[v]-w;sv_dfs(v,u);}
}
signed main()
{scanf("%lld%lld",&n,&root);for(int u,v,w,i=1;i<n;i++)scanf("%lld%lld%lld",&u,&v,&w),add(u,v,w),add(v,u,w);pre_dfs(root,0);sv_dfs(root,0);printf("%lld",ans);return 0;
}

P2014 选课

这题是个裸的可行性01依赖背包
然而我翻了翻题解,发现都没说到点上,所以我决定考前发一波福利

这道题实际上就是有依赖性质的机器分配(P2066),因此,依赖背包实质上就是一个树形分组背包,他与分组背包唯一的差别在于他的转移是树上相邻点的转移

结合代码解释一下为什么叫他分组背包

void dfs(int u)
{
//每个点都是一个组,代表以u为根的子树的最优解for(int i=head[u];i;i=e[i].next){int v=e[i].to;//每次向与他相连的v转移dfs(v);for(int j=V+1;j>=1;j--)//第二重循环枚举体积(第一重循环实质上就是dfs搜索点u)for(int k=0;k<j;k++)//第三重循环枚举组内物品//我们把上多少v内的课作为物品k//第u组第k个物品的体积是k,准确的说,是num[u][k]=k//价值就是f[v][k]f[u][j]=std::max(f[u][j],f[u][j-k]+f[v][k]);}
}

再说几点细节

  • Q:为什么我的第二重循环上限是V+1?
  • A:因为我把0点当做了一个必选的点,这样可以直接从0访问到各森林的根
  • Q:楼上题解的压维是什么意思?没看懂…
  • A:因为是分组背包啊,最优解分组背包可以压维,方案数分组背包不能压维,自己试试就明白了,我测试了一下午,话说如果有人会方案数分组背包的压维,请务必@我一下

AC代码

#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=324,maxm=324;
struct E
{int to,next;E(int to=0,int next=0):to(to),next(next){}
}e[maxn];
int head[maxn],cnt;
inline void add(int u,int v)
{e[++cnt]=(E){v,head[u]};head[u]=cnt;
}
int n,V;
int f[maxn][maxm];
void dfs(int u)
{for(int i=head[u];i;i=e[i].next)//每一组{int v=e[i].to;dfs(v);for(int j=V+1;j>=1;j--)//当前总体积:V+1!!!for(int k=0;k<j;k++)//组内物品f[u][j]=std::max(f[u][j],f[u][j-k]+f[v][k]);}
}
int main()
{scanf("%d%d",&n,&V);memset(f,0xcf,sizeof(f));for(int i=1;i<=V+1;i++)f[0][i]=0;//这题似乎没有用,但毕竟是个可行性背包问题,加上吧 for(int u,i=1;i<=n;i++)scanf("%d%d",&u,&f[i][1]),add(u,i);dfs(0);printf("%d",f[0][V+1]);return 0;
}

P2016 战略游戏

树形最大独立集问题
这题要求所有边都被看到,我们按从父向子树的方向考虑,对于某个点u,它所连接的每条边不被自己看到,就被儿子看到
我们发现对于当前点u的取舍,其实就是一种决策
状态

  • f[u][0]f[u][0]f[u][0]表示不选u点时u的子树所需要的最少士兵数
  • f[u][1]f[u][1]f[u][1]表示选u点时u的子树所需要的最少士兵数

转移

  • f[u][0]+=f[son][1]f[u][0]+=f[son][1]f[u][0]+=f[son][1] 显然不选u点,儿子就必须选
  • f[u][1]+=min(f[son][0],f[son][1])f[u][1]+=min(f[son][0],f[son][1])f[u][1]+=min(f[son][0],f[son][1]) 选u点,儿子选不选就无所谓了,当然要取个min
#include<iostream>
#include<cstdio>
int const maxn=111111;
struct E
{int to,next;E(int to=0,int next=0):to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{e[++cnt]=(E){v,head[u]};head[u]=cnt;
}
int n,kid[maxn][111],ind[maxn];
int f[maxn][2];
void dfs(int u)
{f[u][1]=1,f[u][0]=0;for(int i=1;i<=kid[u][0];i++){int v=kid[u][i];dfs(v);f[u][1]+=std::min(f[v][0],f[v][1]);f[u][0]+=f[v][1];}
}
int main()
{scanf("%d",&n);for(int u,i=1;i<=n;i++){scanf("%d",&u);scanf("%d",kid[u]);for(int j=1;j<=kid[u][0];j++)scanf("%d",kid[u]+j),ind[kid[u][j]]=true;}int root=0;for(int i=0;i<n;i++)if(!ind[i]){root=i;break;}dfs(root);printf("%d",std::min(f[root][0],f[root][1]));return 0;
}

P2276 消防局的设立

树形dp求最大独立集神题,贪心水题

初始状态

  • f[i][0]f[i][0]f[i][0]选自己
  • f[i][1]f[i][1]f[i][1]选儿子
  • f[i][2]f[i][2]f[i][2]选孙子

以上是点i一定被覆盖到的情况


  • f[i][3]f[i][3]f[i][3]儿子一定被覆盖
  • f[i][4]f[i][4]f[i][4]孙子一定被覆盖

以上是i点不一定被覆盖到的情况


转移

  • f[i][0]=1+Σmin(f[j][0...4]);f[i][0] = 1+Σmin(f[j][0...4]);f[i][0]=1+Σmin(f[j][0...4]);
    选自己孙子和儿子就无所谓了,取最小
  • f[i][1]=min(f[k][0]+Σ(j!=k)min(f[j][0...3]));f[i][1] = min( f[k][0] + Σ(j != k)min(f[j][0...3]) );f[i][1]=min(f[k][0]+Σ(j!=k)min(f[j][0...3]));
    选儿子只选一个就好,其余的儿子一定能被覆盖到,但孙子不一定,取最小
  • f[i][2]=min(f[k][1]+Σ(j!=k)min(f[j][0...2]));f[i][2] = min( f[k][1] + Σ(j != k)min(f[j][0...2]) );f[i][2]=min(f[k][1]+Σ(j!=k)min(f[j][0...2]));
    选孙子由“儿子的选儿子”状态转移,儿子和孙子一定能被覆盖到
  • f[i][3]=Σmin(f[j][0...2]);f[i][3] = Σmin(f[j][0...2]);f[i][3]=Σmin(f[j][0...2]);
    儿子自选,所有儿子一定被覆盖
    选孙子,所选孙子的父亲一定被覆盖,其余一定不被覆盖
  • f[i][4]=Σmin(f[j][0...3]);f[i][4] = Σmin(f[j][0...3]);f[i][4]=Σmin(f[j][0...3]);
    儿子自选,所选儿子的儿子一定被
    选孙子,所选孙子的父亲一定被
    选儿子的孙子,所选孙子的祖父一定被

简化状态
我们惊奇的发现,转移可以合并
f[i][j]f[i][j]f[i][j]表示f[i][0...j](j∈[2,4])f[i][0...j] ( j\in[2,4])f[i][0...j](j∈[2,4])


简化后的转移

  • f[i][0]=1+Σf[j][4]f[i][0]=1+Σf[j][4]f[i][0]=1+Σf[j][4]
  • f[i][1]=Σf[j][3]+Σmin(f[k][0]−f[k][3])k∈jf[i][1]=Σf[j][3]+Σmin(f[k][0]-f[k][3]) k\in jf[i][1]=Σf[j][3]+Σmin(f[k][0]−f[k][3])k∈j
  • f[i][2]=Σf[j][2]+Σmin(f[k][1]−f[k][2])k∈jf[i][2]=Σf[j][2]+Σmin(f[k][1]-f[k][2]) k\in jf[i][2]=Σf[j][2]+Σmin(f[k][1]−f[k][2])k∈j
  • f[i][3]=Σmin(f[j][2])f[i][3]=Σmin(f[j][2])f[i][3]=Σmin(f[j][2])
  • f[i][4]=Σmin(f[j][3])f[i][4]=Σmin(f[j][3])f[i][4]=Σmin(f[j][3])

(待处理)P3177 树上染色

(待处理)P4365 秘密袭击

(双倍经验)P3780 苹果树

(待处理)期望DP

P1850 换教室

P1291 百事世界杯之旅

P3412 仓鼠找sugar2

P3400 随机数生成器

P2473 奖励关

动态规划,且学且放弃相关推荐

  1. 动态规划从入门到放弃【1】

    动态规划 分治法 分治法是将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解. 动态规划 动态规划是应用于子问题重叠的情况,即不同的子问题具有公共的子子问题. 最简单 ...

  2. NOI入门级:算法之动态规划

    糖糖讲动态规划算法,找零钱完全背包问题,LeetCode 322 糖糖讲动态规划算法,找零钱完全背包问题,LeetCode 322_哔哩哔哩_bilibili 程序员面试再也不怕动态规划了,看动画,学 ...

  3. 呼声很高的这门语言真的难学吗?

    不少同学很想自学Java,但又不知道从何开始?到底该如何系统地去学?学着学着想放弃了--万事开头难,只要跟着系统的阶段学习方法走,条理会很清晰,学起来会更轻松,其实自学真的没有想象的那么难! 那到底该 ...

  4. Elasticsearch从入门到放弃:人生若只如初见

    Elasticsearch系列开坑啦,入门总是愉快的,学一学再放弃啊. Apache Lucene简介 Lucene基本概念 Apache Lucene是ElasticSearch使用的全文检索库.了 ...

  5. 今年考研又要线上复试?多所大学计算机考研线上复试详情!

    往年考研复试都是线下进行,计算机考研也不例外.本来2020年疫情好转,各种活动都逐渐的开始进行,很多人都认为考研复试也会正常转为线下复试. 但是,临近过年的时候,部分地区又出现了小部分疫情,部分人过年 ...

  6. 成为多编程语言人才的诀窍你想知道吗

    学习一种新的编程语言就像学一门新的语言.相信许多学习两门或多门语言的朋友都会认同,在双语或多语言间转换难免令人混淆.例如,我的母语是英语,在学校里参加了沉浸式法语课程.这一方面使我的英法双语都达到了十 ...

  7. python输出自己的名字_【Python】Python-用大写字母打印你的名字

    http://www.cnblogs.com/xiashengwang/p/4294409.html 与孩子一起编程,有个作业题:用大写字母打印你的名字. CCCC A RRRRR C C A A R ...

  8. 音视频怎样入门?带你入门基础+学习思路

    Android开发的路越走越难,难道真的没有其他出路了? 并没有,一个行业的下降趋势也会带起新的行业,他的本质不会变,技术会稍有改变,本篇我们就来说说音视频行业. 音视频行情分析 1.市场 市场是一个 ...

  9. 我爬取了CSDN总榜TOP10博主的2万多篇文章,看看他们为什么这么火

    前言:受到明佬<CSDN文章漫游者>的启发(←造福C站全体用户,最新版文章漫游者v0.3开放下载,赶快体验吧!),笔者将 CSDN 总榜 TOP10 的大佬们的文章数据爬了下来,分析一波大 ...

最新文章

  1. 三十三、自从我会了Python中的PIL,发现了其实超级简单
  2. Kotlin学习笔记(2)- 空安全
  3. 使用PuTTY、Xshell远程连接Linux,密钥认证连接
  4. Effective C++ 读书笔记(八)
  5. 什么? 搞不定redis分布式锁?
  6. 控制台输出HelloWorld案例
  7. Linux的sys接口和proc接口创建
  8. java 企业级应用设计规范_一步一步理解Java 企业级应用的可扩展性
  9. 今日头条收购锤子?ofo 半月退 24 万户押金;斗鱼索赔主播 1.5 亿元 | 极客头条...
  10. !DOCTYPE html 到底是什么意思?
  11. 关于基础类数据结构的设计想法
  12. 一年中所有节日的排列顺序_中国传统节日有哪些 按顺序排列全部
  13. ZKtime5.0考勤管理系统标准版客户端登录忘记登录密码
  14. android webview浏览器下载文件,Android 浏览器 —— 使用 WebView 实现文件下载
  15. 木马病毒属于什么计算机病毒,下列不属于计算机病毒分类的是()。 A.文件型病毒 B.复合型病毒 C.系统引导病毒 D.木马病毒 - 上学吧找答案(手机版)...
  16. WINDOWS系统还原后原来多个分区合并为一个分区后怎么恢复数据?
  17. 2019 ICPC 上海网络赛 K. Peekaboo
  18. 从从协方差的误差椭圆到PCA
  19. 【学习笔记】NIM游戏与SG函数初探
  20. ETL工具 鱼龙混杂,如何甄选?(开源、包开源、包数据库辅助功能、商用)

热门文章

  1. 如何在系统菜单中添加一个自己的菜单项
  2. 运维人该醒醒了,要变天了
  3. eclipse官方提供的php插件的使用
  4. SQL PIVOT函数进行 行转列
  5. c语言在线测试题目及答案,《C语言考试题库及答案》.doc
  6. 几行代码轻松搞定网页的简繁转换(转载)
  7. 小米开源文件管理器MiCodeFileExplorer-源码研究(0)-初步研究
  8. EXP2 分析PE感染程序
  9. 评价股票博客好坏的指标(第11期)
  10. 郑清oj1191:数星星(结构体专题)