前言
神奇妙妙dp!
一、741. 摘樱桃
cpp
class Solution {
public:
int cherryPickup(vector<vector<int>>& grid) {
int n=grid.size();
//贪心的方法不行!
//对于这种一来一回的问题,往往可以转化成两起点问题
//e.g.一个人一来一回 -> 两个人同步从起点开始走一趟
//"同步"使得不存在有一个格子两个人先后到达
vector<vector<vector<int>>>dp(n,vector<vector<int>>(n,vector<int>(n,-2)));
int ans=dfs(0,0,0,dp,n,grid);
return ans==-1?0:ans;
}
//当前A人来到(a,b),B人来到(c,d)
//由于是同步走的,所以a+b==c+d,所以d=a+b-c
int dfs(int a,int b,int c,
vector<vector<vector<int>>>&dp,int n,vector<vector<int>>&grid)
{
int d=a+b-c;
//道路不存在
if(a==n||b==n||c==n||d==n||grid[a][b]==-1||grid[c][d]==-1)
{
return -1;
}
//因为同步走,所以若A到了,那么B也到了
if(a==n-1&&b==n-1)
{
return grid[a][b];
}
if(dp[a][b][c]!=-2)
{
return dp[a][b][c];
}
//当前格子的收益
int cur=(a==c&&b==d)?grid[a][b]:(grid[a][b]+grid[c][d]);
//往后走的收益
int next=dfs(a+1,b,c+1,dp,n,grid);
next=max(next,dfs(a+1,b,c,dp,n,grid));
next=max(next,dfs(a,b+1,c+1,dp,n,grid));
next=max(next,dfs(a,b+1,c,dp,n,grid));
int ans=-1;
if(next!=-1)
{
ans=next+cur;
}
dp[a][b][c]=ans;
return ans;
}
};
上来的第一道题就提供了一个很牛逼的trick,就是对于这种一来一回问题,可以考虑将其转化成两个人同步从起点出发走一趟。注意,这里"同步"的设置使得不存在一个格子两人先后到达。
之后,考虑定义dp[a][b][c]为当前两人分别位于(a,b)和(c,d)位置,这里因为两人是同步走的,所以走过的曼哈顿距离是一样的,所以第二个人的d是可以通过其他三个坐标计算得到的。之后就是计算一下当前格子的收益,若两人位于同一格子,那么就只能得到一份收益,否则就是两个格子的累加和。然后考虑四种往后的可能性,若有效就计算最大收益然后记忆化即可。
二、青蛙过河
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define INF 1e9
#define INFLL 1e18
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
ll n,x;
cin>>n>>x;
vector<ll>a(n+1);
for(int i=1;i<n;i++)
{
cin>>a[i];
}
//补一个
a[n]=2*x;
//一共跳2x次 -> 有2x个青蛙要跳到对岸
//因为存在单调性:若步长step可以达到,那么大于step的步长一定都可以达到
//所以考虑使用二分答案
//最初,若1~step的石头的累加和大于等于2x,说明1~step的石头可以承载所有青蛙
//之后,若2~step+1的石头累加和大于等于2*x,说明1位置上的青蛙可以跳到2~step+1范围内
//那么对于当前的步长step,考虑设置一个长度为step的滑动窗口
//若窗口内的累加和大于等于2x,说明当前step长度的石头可以承载所有青蛙
//进一步考虑,可以使用双指针
//每次从当前位置l开始往后找最小的r,使得窗口内累加和大于等于2*x
//那么整个过程中窗口的最大值就是所需的步长
auto check=[&](int mid)->bool
{
ll sum=0;
for(int i=1;i<=mid;i++)
{
sum+=a[i];
}
for(int l=1,r=mid+1;l<=n;l++,r++)
{
if(sum<2*x)
{
return false;
}
sum-=a[l];
if(r<=n)
{
sum+=a[r];
}
}
return true;
};
int l=1;
int r=n;
int m;
int ans=0;
while(l<=r)
{
m=l+r>>1;
if(check(m))
{
ans=m;
r=m-1;
}
else
{
l=m+1;
}
}
cout<<ans<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
还是这个trick,对于来回跳2x次的问题,可以转化为有2x个青蛙要跳到对岸。之后,因为存在单调性:若步长step可以达到,那么大于step的步长一定都可以达到,所以考虑使用二分答案。
最初,若1~step的石头的累加和大于等于2x,说明1~step的石头可以承载所有青蛙。之后,若2~step+1的石头累加和大于等于2x,说明1位置上的青蛙可以跳到2~step+1范围内。那么对于当前的步长step,考虑设置一个长度为step的滑动窗口。若窗口内的累加和大于等于2x,说明当前step长度的石头可以承载所有青蛙。
进一步考虑,其实是可以使用双指针优化的。方法就是每次从当前位置l开始往后找最小的r,使得窗口内累加和大于等于2x,那么整个过程中窗口的最大值就是所需的步长。这样就把二分答案的O(logn)也优化掉了。
三、B. The Number of Products
经典cf思维题()
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define INF 1e9
#define INFLL 1e18ll
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int n;
cin>>n;
vector<ll>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//将正数看作0,负数看作1,那么相乘就可以看作异或运算
//所以定义dp[i][0]为子数组以i位置结尾,乘积为正的个数,dp[i][1]为乘积为负的个数
//所以考虑维护前缀异或,那么dp[i][0]就是前缀中pre[j]^pre[i]=0的个数
ll ans1=0,ans0=0;
int zero=1,one=0;
int pre=0;
for(int i=1;i<=n;i++)
{
pre^=(a[i]<0);
if(pre)
{
ans1+=one;
ans0+=zero;
one++;
}
else
{
ans1+=zero;
ans0+=one;
zero++;
}
}
cout<<ans0<<" "<<ans1<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
还是一个trick,对于正负问题,考虑将正数看作0,负数看作1,那么相乘就可以看作异或运算。
所以还是这个常见的状态设计,即定义dp[i][0]为子数组以i位置结尾,乘积为正的个数,dp[i][1]为乘积为负的个数。之后,考虑维护前缀异或和,那么dp[i][0]就是前缀中pre[j]^pre[i]=0的个数。那么这个就可以通过几个变量递推了,注意初始什么都没有时可以看作乘以1,所以zero要设置为1。
四、绝世好题
这个状态设计太逆天了,确实是绝世好题......
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define INF 1e9
#define INFLL 1e18ll
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//定义pre[j]为
//之前的数组成的所有合法的子序列中,最后的数第j位是1的最长的子序列长度
//那么对于当前数是1的位,就可以把之前最长的子序列变得更长
vector<int>pre(32);
for(int i=1;i<=n;i++)
{
int x=a[i];
//自己
int cur=1;
for(int j=0;j<31;j++)
{
if((x>>j)&1)
{
cur=max(cur,pre[j]+1);
}
}
for(int j=0;j<31;j++)
{
if((x>>j)&1)
{
pre[j]=max(pre[j],cur);
}
}
}
int ans=0;
for(int j=0;j<31;j++)
{
ans=max(ans,pre[j]);
}
cout<<ans<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
这个题的思想感觉比较像最长递增子序列,都是用一个结构维护可能性。
首先,题目的要求是,对于生成的子序列,相邻两数至少有一位需要都是1。所以,可以考虑定义pre[j]为,之前的数组成的所有合法的子序列中,最后的数第j位是1的最长的子序列长度。那么对于当前数是1的位,就可以让当前数参与把之前最长的子序列变得更长。那么就可以每次取最大值,找出当前数参与作为子序列最后一位时,能构成的最长子序列长度。然后,同样对于当前数是1的位,就可以去看是否能把pre更新得更长。最后,答案就是pre中每一位是1时的最长子序列长度中的最大值。
五、摆盘子的方法
一共有n个盘子,k种菜,所有盘子排成一排,每个盘子只能放一种菜,要求最多连续两个盘子菜品一样,不允许更长的重复出现,返回摆盘的方案数,对1e9+7取模。1<=n<=1000,1<=k<=1000。
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define INF 1e9
#define INFLL 1e18ll
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
const int MOD=1e9+7;
ll dfs(int i,int k,vector<int>&path)
{
if(i==path.size())
{
int len=1;
for(int j=1;j<path.size();j++)
{
if(path[j-1]==path[j])
{
len++;
}
else
{
len=1;
}
if(len>2)
{
return 0;
}
}
return len>2?0:1;
}
int ans=0;
for(int j=0;j<k;j++)
{
path[i]=j;
ans+=dfs(i+1,k,path);
}
return ans;
}
//暴力解
ll solve1(int n,int k)
{
vector<int>path(n);
return dfs(0,k,path);
}
//dp解
ll solve2(int n,int k)
{
if(n==1)
{
return k;
}
//定义dp[i]为当前来到位置i,且i和i+1位置菜品不一样的合法方案数
//如果要让i和i-1位置的菜一样,那么依赖的就是dp[i-2]*(k-1)
//如果要让i和i-1位置的菜不一样,那么依赖的就是dp[i-1]*(k-1)
//最后来到n位置时
//如果要让n-1和n位置的菜不一样,那么对于n位置选的每一种菜,都可以获得一份dp[n-1],所以依赖的就是dp[n-1]*k
//如果要让n-1和n位置的菜一样,同样对于选的每一种菜,都可以获得一份dp[n-2],所以依赖的就是dp[n-2]*k
vector<ll>dp(n+1);
dp[0]=1;
dp[1]=k-1;
for(int i=2;i<n;i++)
{
dp[i]=((dp[i-1]+dp[i-2])%MOD*(k-1))%MOD;
}
return (dp[n-1]+dp[n-2])%MOD*k%MOD;
}
void test()
{
srand(time(0));
cout<<"S"<<endl;
int testTime=100;
for(int i=1;i<=testTime;i++)
{
int n=rand()%8+1;
int k=rand()%10+1;
ll ans1=solve1(n,k);
ll ans2=solve2(n,k);
if(ans1!=ans2)
{
cout<<"W"<<endl;
}
if(i%10==0)
{
cout<<"Have tested "<<i<<" cases"<<endl;
}
}
cout<<"E"<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
test();
}
return 0;
}
这个题的状态设计也挺妙的。
考虑定义dp[i]为当前来到位置i,且i和i+1位置菜品不一样的合法方案数。
那么如果要让i和i-1位置的菜一样,那么依赖的就是dp[i-2]*(k-1)。此时因为根据定义,dp[i-2]要求了i-2和i-1位置的菜品不一样,所以可以直接和后续接上。而如果要让i和i-1位置的菜不一样,那么依赖的就是dp[i-1]*(k-1)。
此外,还要注意边界的特判。就是最后来到n位置时,如果要让n-1和n位置的菜不一样,此时因为对于n位置选的每一种菜,都可以获得一份dp[n-1],所以依赖的就是dp[n-1]*k,而不是和之前一样的dp[n-1]*(k-1)。如果要让n-1和n位置的菜一样,同样对于选的每一种菜,都可以获得一份dp[n-2],所以依赖的就是dp[n-2]*k。
六、过河
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define INF 1e9
#define INFLL 1e18ll
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int l;
cin>>l;
int s,t,m;
cin>>s>>t>>m;
vector<int>a(m+1);
for(int i=1;i<=m;i++)
{
cin>>a[i];
}
//定义dp[i]为当前来到i位置,经过的最少石子数
//每次依赖之前[s,t]距离的位置,那么若i位置是石头,答案就需要+1
//注意到l很大的,直接这样dp肯定会tle和mle
//又注意到石子的数量是很少的,所以整个路径上石子的分布是很稀疏的
//又因为[s,t]也很小,所以当相邻两块石头相隔很远时
//dp值在中间很长一段距离是不会变的,所以可以考虑进行缩点
//缩点时可以直接乱搞,反正[s,t]很小,随便设一个安全距离即可
sort(a.begin()+1,a.end());
//每次只能跳固定距离
if(s==t)
{
int ans=0;
for(int i=1;i<=m;i++)
{
if(a[i]%s==0)
{
ans++;
}
}
cout<<ans<<endl;
return ;
}
int safe=666;
//对石子重定位
vector<int>where(m+1);
vector<int>stone(safe*m+1);
for(int i=1;i<=m;i++)
{
where[i]=where[i-1]+min(a[i]-a[i-1],safe);
stone[where[i]]=1;
}
l=min(l,where[m]+safe);
vector<int>dp(l+1,INF);
dp[0]=0;
for(int i=1;i<=l;i++)
{
for(int j=max(i-t,0);j<=i-s;j++)
{
dp[i]=min(dp[i],dp[j]+stone[i]);
}
}
int ans=INF;
for(int i=where[m]+1;i<=l;i++)
{
ans=min(ans,dp[i]);
}
cout<<ans<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
这个题其实就涉及到了 缩点 这个比较重要的思想了。
这个题的状态设计还是挺简单的,就是定义dp[i]为当前来到i位置,经过的最少石子数。那么每次依赖之前[s,t]距离的位置dp值的最小值,然后若i位置是石头,答案就需要+1。
但是,注意到桥的长度L很大的,直接这样定义dp肯定会tle和mle。但是又注意到石子的数量是很少的,所以可以想到,整个路径上石子的分布是很稀疏的。又因为[s,t]也很小,所以当相邻两块石头相隔很远时,dp值在中间很长一段距离是不会变的,所以可以考虑进行缩点。
之后,首先特判掉s等于t,即每次只能跳固定距离的情况。然后,在这个题里,缩点时可以直接乱搞,反正[s,t]很小,随便设一个安全距离safe即可。之后就是根据这个安全距离对石子进行重定位。若两颗石子的间距很大时,就认为其间距为safe。所以整个桥的长度L就可以被减小到safe*m,此时就可以正常跑dp了。
现在才发现,其实转移时的枚举行为可以用单调队列优化的。
七、放苹果
经典计数题,不得不说还是左神讲的好。之前遇到这个题的时候去oiwiki上看得头晕脑胀的,现在学左神一讲就懂,左神伟大!!
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
const int MAXN=10+5;
vector<vector<ll>>dp(MAXN,vector<ll>(MAXN,-1));
//dfs(m,n):当前还剩m个苹果,n个盘子,允许有空盘的方法数
ll dfs(int m,int n)
{
//没苹果了 -> 找到一种
if(m==0)
{
return 1;
}
//没盘子了 -> 无效方案
if(n==0)
{
return 0;
}
if(dp[m][n]!=-1)
{
return dp[m][n];
}
int ans;
//盘子数大于苹果树 -> 必然有n-m个空盘
if(n>m)
{
ans=dfs(m,m);
}
else
{
//dfs(m,n-1):不用所有的盘子 -> 来一个空盘
//dfs(m-n,n):必须用所有的盘子 -> 给所有盘子分配一个
ans=dfs(m,n-1)+dfs(m-n,n);
}
dp[m][n]=ans;
return ans;
}
void solve()
{
int m,n;
cin>>m>>n;
cout<<dfs(m,n)<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
这个题的状态定义还好,就是定义dp[m][n]为当前还剩m个苹果,要分到n个盘子里,允许有空盘的方法数。那么首先,若m等于0了,即没苹果了,那么就说明找到了一种。否则,若n等于0了,那就说明苹果没分完,就说明是无效方案。
之后,若n大于m,即盘子数大于苹果数,那么必然会导致空盘的出现。此时就是和把m个苹果放到m个盘子里是等效的,所以直接依赖dp[m][m]。否则,即苹果数大于等于盘子数,那么就可以分两种情况讨论。第一种情况就是可以不用所有盘子,那么就可以先来一个空盘,依赖的就是dp[m][n-1]。第二种情况就是必须用所有的盘子,那么此时就可以先给每个盘子都分配一个苹果,之后再考虑往每个盘子里额外放几个苹果,所以依赖dp[m-n][n]。
八、数的划分
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve1()
{
int n,k;
cin>>n>>k;
//模板:将i个苹果放进j个盒子里,不允许空盒
//首先必须保证每个盒子非空,所以可以考虑先往j个盒子里各放一个
//那么之后就转化成了将i-j个苹果放进j个盒子,此时怎么分配都是合理的了
//所以转移dp[i][j]=sum(p=1~j)dp[i-j][p]
//注意到dp[i-1][j-1]=sum(p=1~j-1)dp[i-j][p]
//因为i和j同减1时i-j不变,但依赖的少了dp[i-j][j]
//所以可以优化成dp[i][j]=dp[i-1][j-1]+dp[i-j][j]
//dp[i][j]:把i分成j个数的方案数
vector<vector<ll>>dp(n+1,vector<ll>(k+1));
dp[0][0]=1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=min(k,i);j++)
{
dp[i][j]=dp[i-1][j-1]+dp[i-j][j];
}
}
cout<<dp[n][k]<<endl;
}
const int MAXN=200+5;
const int MAXK=6+5;
vector<vector<ll>>dp(MAXN,vector<ll>(MAXK,-1));
ll dfs(int n,int k)
{
if(n==0)
{
return 1;
}
if(k==0)
{
return 0;
}
if(dp[n][k]!=-1)
{
return dp[n][k];
}
int ans;
if(k>n)
{
ans=dfs(n,n);
}
else
{
ans=dfs(n,k-1)+dfs(n-k,k);
}
dp[n][k]=ans;
return ans;
}
void solve2()
{
int n,k;
cin>>n>>k;
if(n<k)
{
cout<<0<<endl;
return ;
}
if(n==k)
{
cout<<1<<endl;
return ;
}
//先给每个盘子都分配一个
n-=k;
cout<<dfs(n,k)<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve2();
}
return 0;
}
这就是经典的分拆数问题。
这个问题其实和上个问题把m个苹果放进n个盘子是类似的,唯一不同的就是要求每个盘子不能为空。那么其实可以考虑上来先给每个盘子都分配一个,那么之后就是把m-n个苹果放进n个盘子,允许有空盘了的问题了。那么上来先给n减去k,然后去跑上个题的dfs即可。
再说说以前自己学的时候的思路,定义dp[i][j]为把i个苹果放进j个盘子,不允许有空盘的方案数。首先,根据dp的定义,有转移方程dp[i][j]等于dp[i-j][1~j]的累加和。这是因为把当前i个苹果放进j个盘子,不允许有空盘的方案数,就等于先往j个盘子里放一个苹果,那么还剩下i-j个苹果。之后,就等价于把这些剩下的苹果放进1~j个盘子的方案数之和。进一步观察可以发现,dp[i-1][j-1]等于dp[i-j][1~j-1]的累加和,那么只需要把少的dp[i-j][j]加上即可。
感觉还是左神的思路好懂!
九、最好的部署
一共有n台机器,编号1~n,所有机器排成一排。你只能一台一台地部署机器,你可以决定部署的顺序,最终所有机器都要部署。
给定三个数组no,one和both。其中no[i]表示如果i号机器部署时,相邻没有机器部署,能获得的收益。one[i]表示如果i号机器部署时,相邻有一台机器部署,能获得的收益。both[i]表示如果i号机器部署时,相邻有两台机器部署,能获得的收益。认为第1号和第n号机器不会有两台相邻的机器,返回部署的最大收益。其中1<=n<=1e5,no[i],one[i]和both[i]均为正数。
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
const int MAXN=1e5+5;
int n;
vector<int>no(MAXN);
vector<int>one(MAXN);
vector<int>both(MAXN);
//部署[l,r]范围上的机器
//且l-1和r+1的机器都没部署的最大收益
int dfs(int l,int r,vector<vector<int>>&dp)
{
if(l==r)
{
return no[l];
}
if(dp[l][r]!=-1)
{
return dp[l][r];
}
//由于每台机器既依赖左侧又依赖右侧
//所以考虑"时光倒流"的思想,讨论每台机器最后部署的情况
//第l个机器最后部署 和 第r个机器最后部署
int ans=max(dfs(l+1,r,dp)+one[l],dfs(l,r-1,dp)+one[r]);
//枚举区间内哪台机器最后部署
for(int p=l+1;p<=r-1;p++)
{
ans=max(ans,dfs(l,p-1,dp)+dfs(p+1,r,dp)+both[p]);
}
dp[l][r]=ans;
return ans;
}
//区间dp暴力方法 -> O(n^3)
int solve1()
{
vector<vector<int>>dp(n+1,vector<int>(n+1,-1));
return dfs(1,n,dp);
}
//线性dp正解
int solve2()
{
//dp[i][p]:当前来到第i号机器,p为0表示前一台没部署,p为1表示前一台部署了
vector<vector<int>>dp(n+1,vector<int>(2));
dp[n][0]=no[n];
dp[n][1]=one[n];
for(int i=n-1;i>=1;i--)
{
dp[i][0]=max(dp[i+1][1]+no[i],dp[i+1][0]+one[i]);
dp[i][1]=max(dp[i+1][1]+one[i],dp[i+1][0]+both[i]);
}
return dp[1][0];
}
void randomArray()
{
for(int i=1;i<=n;i++)
{
no[i]=rand()%2000;
one[i]=rand()%2000;
both[i]=rand()%2000;
}
}
void test()
{
srand(time(0));
n=rand()%100+1;
randomArray();
int ans1=solve1();
int ans2=solve2();
if(ans1!=ans2)
{
cout<<"W"<<endl;
cout<<n<<endl;
for(int i=1;i<=n;i++)
{
cout<<no[i]<<" "<<one[i]<<" "<<both[i]<<endl;
}
}
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
init();
cout<<"S"<<endl;
while(t--)
{
test();
}
cout<<"E"<<endl;
return 0;
}
状态机dp(!
考虑定义dp[i][0]为当前来到第i号机器,前一台机器没部署,dp[i][1]为前一台机器部署了。此时,因为前一台机器的状态已经确定了,那么就可以从后往前dp,考虑后一台机器的状态了。所以,初始化时dp[n][0]就是no[n],dp[n][1]就是one[n]。之后,从后往前dp,那么对于dp[i][0],如果先部署当前机器后部署右侧机器,那么就是dp[i+1][1]再加上one[i]。而若先部署右侧机器再部署当前机器,那么就是dp[i+1][0]再加上one[i]。类似地,dp[i][1]依赖的就是dp[i+1][0]+both[i]和dp[i+1][1]+one[i]。
其实区间dp的暴力解也不好想,考虑定义dp[l][r]为部署[l,r]范围上的机器,且l-1和r+1的机器都没部署的最大收益。那么当只剩一个机器时,收益就是no[l]。之后,由于每个机器既依赖左侧又依赖右侧,那么就可以考虑"时光倒流"的方法,讨论每台机器最后部署的情况。所以特判一下端点位置,然后暴力枚举区间内哪个位置最后部署即可。
十、增加限制的最长公共子序列
给定两个字符串s和t,s长度为n,t长度为m,返回s和t的最长公共子序列长度。其中两字符串都由小写字母组成,1<=n<=1e6,1<=m<=1e3。
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
const int MAXN=1e6+5;
const int MAXM=1e3+5;
int n,m;
string s,t;
//经典最长公共子序列dp ->O(n*m)
int solve1()
{
vector<vector<int>>dp(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(s[i]==t[j])
{
dp[i][j]=1+dp[i-1][j-1];
}
else
{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[n][m];
}
//当前每个字符最早出现的位置
vector<int>suf(26);
//从当前位置开始,每个字符下一次出现的位置
vector<vector<int>>nextt(MAXN,vector<int>(26));
vector<vector<int>>dp(MAXM,vector<int>(MAXM));
//对较长的字符串s建立查询结构
void build()
{
for(int j=0;j<26;j++)
{
suf[j]=INF;
}
for(int i=n;i>=0;i--)
{
for(int j=0;j<26;j++)
{
nextt[i][j]=suf[j];
}
if(i>0)
{
suf[s[i]-'a']=i;
}
}
for(int i=0;i<=m;i++)
{
for(int j=0;j<=m;j++)
{
dp[i][j]=-1;
}
}
}
//长度为i的t的前缀串,想和s串形成长度为j的公共子序列
//至少需要s串多长的前缀串才能做到
int dfs(int i,int j)
{
//做不到
if(i<j)
{
return INF;
}
if(j==0)
{
return 0;
}
if(dp[i][j]!=-1)
{
return dp[i][j];
}
int cur=t[i]-'a';
//不要t串中i位置的字符
int ans=dfs(i-1,j);
//必须要t串中i位置的字符
int pre=dfs(i-1,j-1);
if(pre!=INF)
{
//从之前达到的位置pre开始,往后cur字符第一次出现的位置
ans=min(ans,nextt[pre][cur]);
}
dp[i][j]=ans;
return ans;
}
//正解 -> O(m^2)
int solve2()
{
build();
//t串整体能和s串形成多长的子序列
for(int j=m;j>=1;j--)
{
if(dfs(m,j)!=INF)
{
return j;
}
}
return 0;
}
void randomString()
{
s=" ",t=" ";
for(int i=1;i<=n;i++)
{
s+=char(rand()%26+'a');
}
for(int i=1;i<=m;i++)
{
t+=char(rand()%26+'a');
}
}
void test()
{
srand(time(0));
n=rand()%1000+1;
m=rand()%1000+1;
randomString();
int ans1=solve1();
int ans2=solve2();
if(ans1!=ans2)
{
cout<<"W"<<endl;
cout<<n<<endl;
cout<<s<<endl;
cout<<m<<endl;
cout<<t<<endl;
cout<<ans1<<" "<<ans2<<endl;
}
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
cout<<"S"<<endl;
for(int i=1;i<=t;i++)
{
test();
if(i%100==0)
{
cout<<"Have tested "<<i<<" cases"<<endl;
}
}
cout<<"E"<<endl;
return 0;
}
逆天状态设计......
回顾经典的最长公共子序列问题,可以发现复杂度是O(n*m),肯定不行,需要想新的解。
考虑定义dp[i][j]为长度为i的t的前缀串,想和s串形成长度为j的公共子序列,至少需要s串多长的前缀串。首先,如果i小于j,即t的前缀串的长度比子序列长度j小,那么肯定不可能达成,返回无穷大。否则若j等于0长度,那么0长度就可以。之后,分别讨论不要t串i位置的字符和一定要这个字符两个情况。那么如果不要的话就是dp[i-1][j],而如果要的话,若dp[i-1][j-1]有可能达成,那么最短需要的长度,就是从前面达成的长度dp[i-1][j-1]开始往后,去找当前字符第一次出现的位置,这个就是需要达成的最短长度。所以求解时只需要从大往小枚举j,那么第一个能达成的j就是答案。
之后,就需要考虑去构建一个next表,可以快速查从i位置开始往后第一个j字符出现的位置了。那么就可以考虑设置一个suf表存每个字符上一次出现的位置,然后每次先往next表里刷一遍suf,然后更新suf表中当前字符的位置即可。
所以,预处理的复杂度为O(26*n),在dp的过程中由于i小于j时就直接返回了,又因为i是t串的长度,所以复杂度其实是O(m^2)的。
十一、鸡蛋掉落
太变态了这个题......
cpp
class Solution {
public:
int superEggDrop(int k, int n) {
return solve1(k,n);
//return solve2(k,n);
}
//左神方法
int solve1(int k,int n)
{
//dp[i][j]:有i个鸡蛋,只能扔j次
//最多能解决多少层楼的这个问题
vector<vector<int>>dp(k+1,vector<int>(n+1));
//只能扔1次时,不管有多少个鸡蛋,都只能解决一层楼的问题
//只有1个鸡蛋时,只能从下往上一层一层试,所以能解决j层楼的问题
for(int j=1;j<=n;j++)
{
for(int i=1;i<=k;i++)
{
//对于当前有i个鸡蛋,能扔j次的情况
//当选择了第x层楼扔
//若鸡蛋没碎,那么说明f必然在x层上方,那么还有i个鸡蛋,能扔j-1次
//若鸡蛋碎了,那么说明f必然在x层下方,那么还有i-1个鸡蛋,能扔j-1次
//所以此时最大可以解决的层数为dp[i][j-1]+dp[i-1][j-1]+1
dp[i][j]=dp[i-1][j-1]+dp[i][j-1]+1;
//从小到大枚举j时,第一次遇到dp[i][j]>=n能完成时就直接返回
if(dp[i][j]>=n)
{
return j;
}
}
}
return -1;
}
//自己写的暴力解 -> O(n*k*logn)
int solve2(int k,int n)
{
vector<vector<int>>dp(k+1,vector<int>(n+1,-1));
int ans=dfs(k,n,dp);
return ans;
}
//dfs(k,n):当前还剩k个鸡蛋,要测n层楼的最小操作数
int dfs(int k,int n,vector<vector<int>>&dp)
{
if(n==0)
{
return 0;
}
if(k==1)
{
return n;
}
if(dp[k][n]!=-1)
{
return dp[k][n];
}
int l=1;
int r=n;
int m;
int ans;
while(l<=r)
{
m=(l+r)>>1;
//没碎 -> 去更高的楼层
int p1=dfs(k,n-m,dp);
//碎了 -> 去下一层
int p2=dfs(k-1,m-1,dp);
//选最大的
if(p1<=p2)
{
ans=m;
r=m-1;
}
if(p1>=p2)
{
ans=m;
l=m+1;
}
}
dp[k][n]=max(dfs(k,n-ans,dp),dfs(k-1,ans-1,dp))+1;
return dp[k][n];
}
};
先说左神方法,考虑定义 dp[i][j] 为有 i 个鸡蛋,只能扔 j 次,最多能解决多少层楼的这个问题。首先,只能扔1次时,不管有多少个鸡蛋,都只能解决一层楼的问题。而只有1个鸡蛋时,只能从下往上一层一层试,所以能解决 j 层楼的问题。
之后,对于当前有 i 个鸡蛋,能扔 j 次的情况,当选择了第 x 层楼扔,若鸡蛋没碎,那么说明 f 必然在 x 层上方,那么还有 i 个鸡蛋,能扔 j-1 次。而若鸡蛋碎了,那么说明 f 必然在 x 层下方,那么还有 i-1 个鸡蛋,能扔 j-1 次。所以此时最大可以解决的层数为 dp[i][j-1]+dp[i-1][j-1]+1。那么在从小到大枚举 j 时,第一次遇到 dp[i][j] 大于等于 n,即能完成时就可以直接返回了。
再说说我自己的方法,考虑定义dp[k][n]为当前还剩 k 个鸡蛋,要测 n 层楼的最小操作数。那么若 n 等于0了答案就是0,若 k 等于1只剩一个鸡蛋了答案就是n。之后,由于存在单调性,即楼层数越大扔的次数越多。又因为是要求最大值最小,所以每次考虑二分在 m 层楼扔,每次考虑碎了和没碎两种情况。若碎了的操作数更大,那么就去更低的楼层二分,否则就去更高的楼层二分。最后带着结果再跑一次取最大值即可。
十二、E - Average and Median
不亏是abc,教育意义是真的强!
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
typedef long long ll;
typedef pair<int,int> pii;
typedef pair<ll,ll> pll;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//dp[i][0]:当前来到i位置,可以要也可以不要的子序列最大累加和
//dp[i][1]:当前来到i位置,必须要的子序列最大累加和
vector<vector<double>>dp1(n+1,vector<double>(2));
vector<double>help1(n+1);
auto DP1=[&]()->double
{
dp1[0][0]=dp1[1][0]=0;
for(int i=1;i<=n;i++)
{
dp1[i][0]=max(help1[i]+dp1[i-1][0],dp1[i-1][1]);
dp1[i][1]=help1[i]+dp1[i-1][0];
}
return max(dp1[n][0],dp1[n][1]);
};
//能否选择一个子序列,使得最大平均值大于等于x
auto check1=[&](double x)->bool
{
for(int i=1;i<=n;i++)
{
help1[i]=(double)a[i]-x;
}
return DP1()>=0;
};
//找最大平均值
//若最小值为l,最大值为r,那么平均值必然在这个范围内
//假设平均值为m,考虑让所有数字都减去m
//若子序列最大累加和大于等于0,就说明最大平均值至少为m
auto average=[&]()->double
{
double l=0;
double r=INF;
for(int i=1;i<=n;i++)
{
l=min(l,(double)a[i]);
r=max(r,(double)a[i]);
}
double eps=1e-5;
double m;
double ans;
while(l+eps<=r)
{
m=(l+r)/2;
if(check1(m))
{
ans=m;
l=m;
}
else
{
r=m;
}
}
return ans;
};
vector<int>sorted(n+1);
for(int i=1;i<=n;i++)
{
sorted[i]=a[i];
}
sort(sorted.begin()+1,sorted.end());
vector<vector<int>>dp2(n+1,vector<int>(2));
vector<int>help2(n+1);
auto DP2=[&]()->int
{
dp2[0][0]=dp2[1][0]=0;
for(int i=1;i<=n;i++)
{
dp2[i][0]=max(help2[i]+dp2[i-1][0],dp2[i-1][1]);
dp2[i][1]=help2[i]+dp2[i-1][0];
}
return max(dp2[n][0],dp2[n][1]);
};
//能否选择一个子序列,使得大于等于x的个数在一半以上
auto check2=[&](int x)->bool
{
for(int i=1;i<=n;i++)
{
help2[i]=a[i]>=x?1:-1;
}
return DP2()>0;
};
//找最大中位数
//假设平均值为m,考虑让大于等于m的数为+1,小于的数为-1
//若子序列最大累加和大于0,说明最大中位数至少为m
auto median=[&]()->int
{
int l=0;
int r=n-1;
int m;
int ans=-1;
while(l<=r)
{
m=l+r>>1;
if(check2(sorted[m]))
{
ans=sorted[m];
l=m+1;
}
else
{
r=m-1;
}
}
return ans;
};
cout<<average()<<endl;
cout<<median()<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
首先,考虑最大平均值的情况。那么若最小值为 l,最大值为 r,那么平均值必然在这个范围内。这里有一个很常见的trick,假设平均值为 m,考虑让所有数字都减去 m。若子序列最大累加和大于等于0,就说明最大平均值至少为m。所以就可以考虑去二分答案,每次先把填 help 数组,然后去跑子序列 check。check 就是考虑定义dp[i][0]为当前来到 i 位置,可以要也可以不要的子序列最大累加和,dp[i][1]为当前来到 i 位置,必须要的子序列最大累加和。那么最后判断一下子序列最大累加和,然后继续二分即可。
对于最大中位数的情况,还是一个常见的 trick,假设平均值为 m,考虑让大于等于 m 的数为+1,小于的数为-1。那么若子序列最大累加和大于0,说明子序列最大中位数大于等于为 m。那么就还是考虑去二分答案,每次 check 时还是先填help数组,然后去子序列 dp 即可。
十三、将珠子放入背包中
cpp
class Solution {
public:
typedef long long ll;
long long putMarbles(vector<int>& weights, int k) {
int n=weights.size();
//分析每种划分方案最终价值的构成
//首先可以发现不管怎么划分,开头和结尾的价值一定会被算入
//之后,产生贡献的珠子,一定位于划分点的两侧
//存在n-1个可选择的划分点,统计每个划分点两侧珠子的贡献并排序
//那么最小的方案就是选最小的k-1个划分点,最大的方案就是选最大的k-1个划分点
vector<ll>vals;
for(int i=0;i<n-1;i++)
{
vals.push_back(weights[i]+weights[i+1]);
}
sort(vals.begin(),vals.end());
ll minn=0;
for(int i=0;i<k-1;i++)
{
minn+=vals[i];
}
ll maxx=0;
for(int i=vals.size()-1,j=0;j<k-1;i--,j++)
{
maxx+=vals[i];
}
return maxx-minn;
}
};
这个题倒是感觉还好(?
考虑分析每种划分方案最终价值的构成,首先可以发现不管怎么划分,开头和结尾的价值一定会被算入。之后可以发现,题意就是说产生贡献的珠子一定位于划分点的两侧。划分为 k 个区间就相当于在 n-1 个可选择的划分点中选 k-1 个,那么就可以考虑统计每个划分点两侧珠子的贡献并排序。那么最小的方案就是选最小的 k-1 个划分点,最大的方案就是选最大的 k-1 个划分点。
总结
还是多见题多写题多积累,加油!