日常wa点:
1.dp时for的i是从大到小还是从小到大,注意是否影响后续性质。1
2.单调队列如果值受到i的影响,那么应该把结果都存下来,而不是只单纯考虑最有值,因为有可能后面进来点会超前面。1
3.基环树注意多个森林。1
4.DP[i]=DP[i-j*w[i]]+xxx可以变成dp[i]=dp[j+w[i]*k]+xxx的形式
5.使用1<<n,的时候注意如果n可以大于30,记得1要加LL
6.插头dp适用:超小数据范围,网格图,连通性。
填表法就是利用状态转移方程和上一个状态来推导出现在的状态(相当于知道已知条件,将答案填入)
刷表法就是利用当前的状态,把有关联的下一状态都推出来。
基环树DP:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int N = 1e6+10; //基环树中点的个数
const int E = 1e6+10; //基环树中无向边的个数
struct Edge{int next,to;}e[E*2];
int head[N], cnt;
bool vis[N];
int val[N], ringPt1, ringPt2, not_pass; //ringPt1,ringPt2 -> not_pass边的端点
long long dp[2][N];
void add(int u,int v){
e[++cnt].next = head[u];
e[cnt].to = v;
head[u] = cnt;
}
void getDP(int rt, int fa) {
dp[0][rt] = 0, dp[1][rt] = val[rt];
for(int i=head[rt];i!=-1;i=e[i].next) {
if(e[i].to == fa) continue;
if(i == not_pass || i == (not_pass^1)) continue;
getDP(e[i].to, rt);
//具体树形dp策略(根据实际修改)
dp[0][rt] += max(dp[0][e[i].to], dp[1][e[i].to]);
dp[1][rt] += dp[0][e[i].to];
}
}
void dfs(int rt, int fa) {
vis[rt] = 1;
for(int i=head[rt];i!=-1;i=e[i].next) {
if(e[i].to == fa) continue;
if(!vis[e[i].to]) dfs(e[i].to, rt);
else {
//记录基环上一条特定边的标号
//e[not_pass]为该边
//e[not_pass]^1为该边的反向边
not_pass = i;
ringPt1 = e[i].to; ringPt2 = rt;
}
}
}
void init() {
memset(head,-1,sizeof(head));
cnt = 1; //为配合基环边及其反向边的记录,特将其初始化为1
memset(vis,0,sizeof(vis));
}
int main() {
int i,j,k,f1,f2,f3,f4,t1,t2,t3,t4,m;
int T;
//freopen("in","r",stdin);
init();
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d %d",&t1,&t2);
val[i]=t1;
add(i,t2);add(t2,i);
}
long long ans = 0 ;
long long qq;
for(int i=1;i<=n;i++){
if(vis[i]==1)continue;
dfs(i, -1); //从i开始搜完这一棵树,
getDP(ringPt1, -1); //pt1是尾巴
qq= dp[0][ringPt1];
getDP(ringPt2, -1);
qq=max(qq,dp[0][ringPt2]);
ans += qq;
}
printf("%lld\n",ans);
}
把一个环取出来(这里每个cir就表示为一个完整的环):
for(int i = 1; i <= n; i++){
if(!vis[i]){
top = 0;
getCir(i);
}
}
void getCir(int u){
if(vis[u] == 1) return ;
if(vis[u] == -1){//为-1的时候表示为环,把环记录下来 ,cntCir表示有多条环
cntCir++;
for(int i = top; i >= 1; i--){
// cout <<cntCir <<" "<<sta[i] <<endl;
cir[cntCir].pb(sta[i]);
vvis[sta[i]] = 1;
if(sta[i] == u) break; //找到标记的点,并且回去也就仅仅回到这个标记的点
}
return;
}
vis[u] = -1;
sta[++top] = u;
getCir(f[u]);
top--;
vis[u] = 1;
}
状态压缩二进制:
枚举一个集合的全部子集合。
sub=sup; //枚举sup的所有子集合
do{
sub=(sub-1)⊃
}while(sub!=sup);
枚举个数为i个1的全部集合。
comb=(1<<i)-1;
while(comb<1<<n){
x=comb&-comb,y=comb+x;
comb=((comb&~y)/x>>1)|y;//这一行赋值之前为每次的答案
}
多重背包单调队列优化,之前就-k*v[i],后面k表大的时候再加k*v[i]是这个差值在num[i]个数的范围内,
因为每次都把值存入list1中,所以顺序是正确的:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+7;
int w[maxn],v[maxn],num[maxn];
int list1[maxn];
int val[maxn];
int dp[maxn],deq[maxn];
int main(){
int i,j,k,f1,f2,f3,f4,t1,t2,t3,t4,n,m,T;
//freopen("in.txt","r",stdin);
//freopen("out1.txt","w",stdout);
scanf("%d\n",&T);
while(T--){
memset(dp,0,sizeof(dp));
scanf("%d %d",&n,&m);
for(i=1;i<=m;i++){
scanf("%d %d %d",&w[i],&v[i],&num[i]);
}
int s,t;
for(i=1;i<=m;i++){
for(j=0;j<w[i];j++){
s=t=0; //t为头,s为尾,都不代表数字,期间的才为内容
for(k=0;j+k*w[i]<=n;k++){//s表示的为最优内容,刚开始为0
val[k]=dp[j+k*w[i]]-k*v[i];//也就表示从0开始获取
while(t>s&&val[k]>=val[list1[t-1]]){//t-1表示头,比其优就挤出来
t--;
}
deq[t]=k; //从0开始,因为s是第一个结果,不从0开始不知道s表示什么
list1[t++]=k;
dp[j+k*w[i]]=max(dp[j+k*w[i]],val[list1[s]]+k*v[i]);
while(t>s&&k-deq[s]>=num[i]){ //每次都是值会删除一个值
s++;
}
}
}
}
printf("%d\n",dp[n]);
}
return 0;
}
poj3709,斜率优化:
题意:给一个序列,每个序列为递增序列。每次可以把一个数字的值-1,问最少需要几次操作可以把这个序列做到每个数字至少有k个。
显然dp,dp[i]表示i以及之前的全部处理的最小代价。
那么dp[i]=dp[j]+sum[i]-sum[j-1]-a[j]*(i-j+1),这里j表示被选择作为值的那个下标,那么j的范围是1<=j<=(i-k+1)
这里可以转换为dp[i]=sum[i]-a[j]*i+a[j]*j-a[j]-sum[j-1],化成这么一个形式,有i的相当为x,a[j]为斜率。
斜率显然具有单调性,这里就相当于y=kx+b,b可以是包含有j的函数。
具有单调性,找最小值,那么就画出实际的直线判断取值,这里注意乘一个负数要改变符号
ll check(ll a,ll b,ll c){
ll a1,a2,a3,b1,b2,b3;
a1=-A[a];b1=dp[a-1]-sum[a]+a*A[a];
a2=-A[b];b2=dp[b-1]-sum[b]+b*A[b];
a3=-A[c];b3=dp[c-1]-sum[c]+c*A[c];
return (a2-a1)*(b3-b2)>=(b2-b1)*(a3-a2);//-斜率单调递减,+斜率单调增
}
ll f(ll x,ll b){
return -A[b]*x-(1-b)*A[b]-sum[b-1]+dp[b-1];
}
int main(){
ll i,j,k,f1,f2,f3,f4,t1,t2,t3,t4,n,m,T;
//freopen("in.txt","r",stdin);
//freopen("out1.txt","w",stdout);
scanf("%lld\n",&T);
while(T--){
scanf("%lld %lld",&n,&m);
for(i=1;i<=n;i++){
scanf("%lld",&A[i]);
sum[i]=sum[i-1]+A[i];
}
for(i=1;i<m+m;i++){
dp[i]=sum[i]-i*A[1];
}
ll s,t;
s=t=0;
for(i=m+m;i<=n;i++){
while(t>s+1&&check(list1[t-2],list1[t-1],i-m+1)){//t>s+1表示队列中至少两个数字 {
t--; //这根线被踢出来
}
list1[t++]=i-m+1;
while(t>s+1&&f(i,list1[s])>=f(i,list1[s+1])){ //单调队列没有长度限制,这里只是对比谁更优
s++;//更优的话,之前的就不要了
} //两根及其以上直线才有对比,因为目前这根已经存进去了
dp[i]=sum[i]+f(i,list1[s]);
}
printf("%lld\n",dp[n]);
}
return 0;
}
插头dp(求能网格中能满足成环的种数):
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL LIM=300005,Has=299989;
LL n,m,e1,e2,las,now,tt;LL ans;
LL mp[15][15],bin[15],tot[2];LL js[2][LIM];
LL h[300005],a[2][LIM],ne[LIM];
//tot:状态总数,js:该状态的方案总数,a:各种状态
void ins(LL zt,LL num) {//卓越的哈希技术
LL tmp=zt%Has+1;
for(LL i=h[tmp];i;i=ne[i])
if(a[now][i]==zt) {js[now][i]+=num;return;}
ne[++tot[now]]=h[tmp],h[tmp]=tot[now];
a[now][tot[now]]=zt,js[now][tot[now]]=num;
}
void work(){
tot[now]=1,js[now][1]=1,a[now][1]=0;//初始状态为0,个数为1
for(LL i=1;i<=n;++i){
for(LL j=1;j<=tot[now];++j) //状态二进制最左边为最小一位,当换行的时候最后一个必定为0
a[now][j]<<=1;//则换行的时候最左边那个新的线为0
for(LL j=1;j<=m;++j) {
las=now,now^=1;
memset(h,0,sizeof(h)),tot[now]=0;
// cout <<"----------------------"<<endl;
for(LL k=1;k<=tot[las];++k) {
LL zt=a[las][k],b1=(zt>>(j-1))%2,b2=(zt>>j)%2;
// cout <<i<<" "<<j<<" status "<<zt <<" "<<b1<<" "<<b2<<endl;
//提取关键格子上的两段轮廓线状态,zt表示上一个状态,
LL num=js[las][k]; //这个状态的数量
if(!mp[i][j]){
if(!b1&&!b2)//1个障碍可以有的状态为原状态无右和下的插头
// cout <<"障碍" << "++" <<zt<< endl;
ins(zt,num); //表示这里是一个障碍
}else if(!b1&&!b2){//对于这个状态,只有右下的状态可以选择,但是可以选的条件是右与下均为空地
if(mp[i+1][j]&&mp[i][j+1]){
ins(zt+bin[j-1]+bin[j],num);//转移为插头的状态j-1与j各出现一个
//cout << "00++ " <<zt+bin[j-1]+bin[j] << " "<<num <<endl;
}
}else if(!b1&&b2){ //b1为0,但是b2存在
if(mp[i][j+1]){
ins(zt,num); //转移成连右边的插头
// cout << "01-right " <<zt << " "<<num <<endl;
}
if(mp[i+1][j]){
// cout << "01-botton" << zt-bin[j]+bin[j-1] <<" "<< num <<endl;
ins(zt-bin[j]+bin[j-1],num); //转移成下面的插头
}
}else if(b1&&!b2) {
if(mp[i][j+1]){
ins(zt-bin[j-1]+bin[j],num);//由于左边转移到右边
//cout << "10-right" << zt-bin[j-1]+bin[j] <<" "<< num <<endl;
}if(mp[i+1][j]){
// cout << "10-botton" << zt <<" "<<num <<endl;
ins(zt,num);//转移到下面,状态不改变
}
}else if(b1==1&&b2==1){ //因为少了一个,也就是2个联通块匹配在了一次,那么状态中有一个独立的联通块就改为不独立了
ins(zt-bin[j-1]-bin[j],num);
//cout << "11 " << zt-bin[j-1]-bin[j] <<" "<<num <<endl;
}
}
}
}
}
char s1[1000];
int main(){
//freopen("in.txt","r",stdin);
//freopen("out1.txt","w",stdout);
LL len1,len2,len3;
LL T,i,j;
scanf("%lld",&T);
LL num=0;
bin[0]=1;for(i=1;i<=14;++i)bin[i]=bin[i-1]<<1;
while(T--){
num++;
scanf("%lld%lld",&n,&m);
now=0;
for(i=1;i<=n;i++){
for(j=1;j<=m;j++){
scanf("%lld",&mp[i][j]);
}
}
work();
LL res=0;
for(LL k=1;k<=tot[now];++k) {
LL zt=a[now][k];
if(zt==0)res+=js[now][k];
}
printf("Case %lld: There are %lld ways to eat the trees.\n",num,res);
}
return 0;
}
多个单调队列,记录数组s[],t[]分别表示每个单调队列的位置:
#include<bits/stdc++.h>
using namespace std;
const int maxn=2005;
int min1[maxn];
struct tt1{
int key,r;
};
tt1 q2[maxn][2005];
int dp[2005][2005];
vector<int>q3[maxn];
int s[2005];
int t[2005];
int main(){
int i,j,k,f1,f2,f3,f4,t1,t2,t3,t4,n,m;
int T,K,num;
//freopen("in.txt","r",stdin);
scanf("%d",&T);num=0;
while(T--){
scanf("%d %d %d",&n,&m,&K);
memset(s,0,sizeof(s));
memset(t,0,sizeof(t));
memset(min1,0,sizeof(min1));
for(i=1;i<=K+1;i++)
q2[i][0].key=q2[i][0].r=0;
for(i=1;i<=K;i++)
for(j=1;j<=n;j++)
dp[i][j]=0;
for(i=1;i<=n;i++){
q3[i].clear();
}
num++;
printf("Case #%d: ",num);
for(i=1;i<=m;i++){
scanf("%d %d",&t1,&t2);
q3[t1].push_back(t2);
}
int min2=0;
int res=0;
for(i=1;i<=n;i++){
for(j=0;j<q3[i].size();j++){
int f1=q3[i][j];
if(f1<min2)continue;
min2=f1;
for(k=K;k>=1;k--){
dp[k][f1]=max(dp[k][f1],min1[k-1]+f1-i+1);
dp[k][f1]=max(dp[k][f1],q2[k-1][s[k-1]].key-max(0,q2[k-1][s[k-1]].r-i+1)+f1-i+1);
res=max(res,dp[k][f1]);
if(t[k]==0||dp[k][f1]>=q2[k][t[k]-1].key){
while(t[k]>s[k]&&q2[k][t[k]-1].key-max(0,q2[k][t[k]-1].r-i+1)
<=dp[k][f1]-max(0,f1-i+1)){
t[k]--;
}
q2[k][t[k]].key=dp[k][f1];
q2[k][t[k]].r=f1;
t[k]++;
}
}
}
for(j=1;j<=K;j++){
min1[j]=max(min1[j],dp[j][i]);
}
f2=i+1;
for(k=1;k<=K;k++)
while(t[k]>t[k]&&q2[k][s[k]].key-max(0,q2[k][s[k]].r-f2+1)
<=q2[k+1][s[k+1]].key-max(0,q2[k+1][s[k+1]].r-f2+1))
s[k]++;
}
printf("%d\n",res);
}
return 0;
}
数位DP板子(不要4和62):
ll dfs(ll len,ll if4,ll if6,bool limit){
if(if4) return 0;
if(len==0)return 1;
if(!limit&&dp[len][if6])return dp[len][if6]; //,没有限制长度并且后面记录过了
int up_bound,cnt=0;
if(limit)
up_bound=digit[len];
else
up_bound=9;
for(int i=0;i<=up_bound;i++){
if(if6&&i==2)continue; //处理掉多的部分
cnt+=dfs(len-1,i==4, i==6,limit&&i==up_bound);
}
if(!limit)dp[len][if6]=cnt; //注意这里需要不为limit,不然后面会有限制
return cnt;
}
ll solve(ll x){
int k=0;
while(x){
digit[++k]=x%10;
x/=10;
}
dfs(k,false,false,true);
}
数位DP(hdu6148):
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3+7;
typedef long long ll;
const int mod=1e9+7;
int dp[maxn][13][2];//目前到第i位,上一位为j,目前是只能往上1,上下均可为0
int digit[maxn];
int dfs(int len,int num1,int mark,bool limit){
if(len==0)return 1; //不管是什么数字进来,这里的结果就是1
if(!limit&&dp[len][num1][mark])return dp[len][num1][mark]; //,没有限制长度并且后面记录过了
int up_bound,cnt=0;
up_bound=limit?digit[len]:9;
for(int i=0;i<=up_bound;i++){
if(num1==11&&i==0){ //这么写是把前导0特殊处理
cnt=(cnt+dfs(len-1,11,0,limit&&i==up_bound))%mod;
continue;
}
if(i<num1&&mark)continue;
if(i<=num1)
cnt+=dfs(len-1,i,mark,limit&&i==up_bound);
else
cnt+=dfs(len-1,i,1,limit&&i==up_bound);
cnt%=mod;
}
if(!limit)dp[len][num1][mark]=cnt; //注意这里需要不为limit,不然后面会有限制
return cnt;
}
char s1[maxn];
int solve(){
int k=0;
int len1=strlen(s1);
for(int i=1;i<=len1;i++){
digit[len1-i+1]=s1[i-1]-'0';
}
return dfs(len1,11,0,true);
}
int main(){//V形山
int i,j,k,f1,f2,f3,f4,t1,t2,t3,t4,n,m,T;
//freopen("in.txt","r",stdin);
cin >>T;
while(T--){
//memset(dp,0,sizeof(dp)); //对于数位dp,有时候这里可以省略
cin >> s1;
int res=solve();
cout <<(res+mod-1)%mod<<endl;
}
}
区间DP(四边形不等式):
满足两个性质
1、区间包含的单调性:如果对于 i≤i'<j≤j',有 w(i',j)≤w(i,j'),那么说明w具有区间包含的单调性。(可以形象理解为如果小区间包含于大区间中,那么小区间的w值不超过大区间的w值)
2、四边形不等式:如果对于 i≤i'<j≤j',有 w(i,j)+w(i',j')≤w(i',j)+w(i,j'),我们称函数w满足四边形不等式。(可以形象理解为两个交错区间的w的和不超过小区间与大区间的w的和)
下面给出两个定理:
1、如果上述的 w 函数同时满足区间包含单调性和四边形不等式性质,那么函数 m 也满足四边形不等式性质
我们再定义 s(i,j) 表示 m(i,j) 取得最优值时对应的下标(即 i≤k≤j 时,k 处的 w 值最大,则 s(i,j)=k)。此时有如下定理
2、假如 m(i,j) 满足四边形不等式,那么 s(i,j) 单调,即 s(i,j)≤s(i,j+1)≤s(i+1,j+1)。
然后存一个s[i][j]表示这个区间最优值的下标,然后以后每次就k就不用枚举了
using namespace std;
int dp1[105][105];
int dp2[105][105];
int sum[105];
int qq[105];
int s[105][105];
int main(){
freopen("in.txt","r",stdin);
int i,j,k,f1,f2,f3,f4,t1,t2,t3,t4,n,m;
cin >> n;
for(i=1;i<=n;i++){
cin >> sum[i];
sum[i]+=sum[i-1];
}
int len1;
for(i=0;i<=n+1;i++)s[i][i]=i;
for(len1=1;len1<n;len1++){
for(i=1;i<n;i++){
j=i+len1;
if(j>n)break;
dp1[i][j]=1e9+7;
for(k=s[i][j-1];k<=s[i+1][j];k++){
if(dp1[i][j]>dp1[i][k]+dp1[k+1][j]){
dp1[i][j]=dp1[i][k]+dp1[k+1][j];
s[i][j]=k;
}
}
dp1[i][j]+=sum[j]-sum[i-1];
}
}
for(i=0;i<=n+1;i++)s[i][i]=i;
for(len1=1;len1<n;len1++){
for(i=1;i<n;i++){
j=i+len1;
if(j>n)break; //总长度
//for(k=i;k<j;k++)//枚举中间的k
//dp[i][k],dp[k+1][j]
for(k=s[i][j-1];k<=s[i+1][j];k++){
if(dp2[i][j]<dp2[i][k]+dp2[k+1][j]){
dp2[i][j]=dp2[i][k]+dp2[k+1][j];
s[i][j]=k;
}
}
dp2[i][j]+=sum[j]-sum[i-1];
}
}
cout << dp1[1][n] << endl;
cout << dp2[1][n] << endl;
return 0;
}