前言
现在确实能感觉到 dp 能力有了不小的提升,感谢左神!
一、区间和的个数
太阴了这个题,思路一分钟,调试一小时......
cpp
template<typename T>
struct BIT
{
vector<T>tree;
BIT(int n)
{
tree.resize(n);
}
int lowbit(int i)
{
return i&-i;
}
void add(int i,T v)
{
while(i<tree.size())
{
tree[i]+=v;
i+=lowbit(i);
}
}
T sum(int i)
{
T ans=0;
while(i>0)
{
ans+=tree[i];
i-=lowbit(i);
}
return ans;
}
T query(int l,int r)
{
return sum(r)-sum(l-1);
}
};
typedef long long ll;
class Solution {
public:
int countRangeSum(vector<int>& nums, int lower, int upper) {
int n=nums.size();
//对前缀和离散化
vector<ll>sorted(n+1);
for(int i=1;i<=n;i++)
{
sorted[i]=sorted[i-1]+nums[i-1];
}
sort(sorted.begin()+1,sorted.end());
int len=1;
for(int i=2;i<=n;i++)
{
if(sorted[len]!=sorted[i])
{
sorted[++len]=sorted[i];
}
}
//找小于等于v的最右位置
auto bs=[&](ll v)->int
{
int l=1;
int r=len;
int m;
int ans=0;
while(l<=r)
{
m=l+r>>1;
if(sorted[m]<=v)
{
ans=m;
l=m+1;
}
else
{
r=m-1;
}
}
return ans;
};
BIT<ll>tree(len+1);
ll sum=0;
int ans=0;
for(int i=0;i<n;i++)
{
sum+=nums[i];
if(lower<=sum&&sum<=upper)
{
ans++;
}
//[sum-upper,sum-lower]这个范围里的个数
//小于等于sum-lower的个数 减去 小于等于sum-upper-1的个数
int left=bs(sum-upper-1);
int right=bs(sum-lower);
ans+=tree.sum(right)-tree.sum(left);
int idx=bs(sum);
tree.add(idx,1);
}
return ans;
}
};
对于区间和问题,肯定还是转化成前缀和来解决。而对于区间计数问题,肯定还是考虑固定一个端点,然后快速求合法的另一个端点的个数。所以对于当前来到的位置 i,前缀和为 sum,如果要求区间和在 [l,r] 范围内,那么就是查之前在 [sum-r,sum-l] 范围内的前缀和个数,这个可以通过二分来解决。那么范围查就可以通过树状数组解决了,只需要一开始对前缀和离散化即可。
二、平衡子序列的最大和
cpp
typedef long long ll;
const ll INF=1e18;
template<typename T>
struct BIT
{
vector<T>tree;
BIT(int n,T v=0)
{
tree.resize(n,v);
}
int lowbit(int i)
{
return i&-i;
}
void update(int i,T v)
{
while(i<tree.size())
{
tree[i]=max(tree[i],v);
i+=lowbit(i);
}
}
T query(int i)
{
T ans=-INF;
while(i>0)
{
ans=max(ans,tree[i]);
i-=lowbit(i);
}
return ans;
}
};
class Solution {
public:
long long maxBalancedSubsequenceSum(vector<int>& nums) {
int n=nums.size();
//式子nums[i]-nums[j]>=i-j,可以转化为nums[i]-i>=nums[j]-j
//所以可以只关注nums[i]-i这个值
//对于当前值v,能产生子序列的就是前面值小于等于v的位置中的最大值
//所以可以考虑用树状数组维护最大值
//离散化
vector<ll>sorted(n+1);
for(int i=1;i<=n;i++)
{
sorted[i]=nums[i-1]-(i-1);
}
sort(sorted.begin()+1,sorted.end());
int len=1;
for(int i=2;i<=n;i++)
{
if(sorted[len]!=sorted[i])
{
sorted[++len]=sorted[i];
}
}
auto bs=[&](ll v)->int
{
int l=1;
int r=len;
int m;
int ans=0;
while(l<=r)
{
m=(l+r)>>1;
if(sorted[m]>=v)
{
ans=m;
r=m-1;
}
else
{
l=m+1;
}
}
return ans;
};
//维护最大值的树状数组
BIT<ll>tree(len+1,-INF);
for(int i=0;i<n;i++)
{
int rank=bs(nums[i]-i);
ll pre=tree.query(rank);
//之前的最大值都小于0 -> 自己单独
if(pre<0)
{
tree.update(rank,nums[i]);
}
else
{
tree.update(rank,pre+nums[i]);
}
}
return tree.query(len);
}
};
不难想到,对于这个式子 nums[i]-nums[j]>=i-j,可以转化为 nums[i]-i>=nums[j]-j,那么就可以只关注 nums[i]-i 这个值 v 了。之后,对于当前值 v,能产生子序列的就是前面值小于等于 v 的位置中的最大值,所以可以考虑用树状数组维护最大值。因为值域很大,所以还需要离散化一下。
之后,和子数组最大累加和一样,若之前的最大值的收益都小于 0,那么就让当前位置单独构成子序列即可。否则,就去树状数组里更新最大值即可。
三、方伯伯的玉米田
逆天状态设计......
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 MAXV=5500+5;
const int MAXK=500+5;
vector<vector<int>>tree(MAXV,vector<int>(MAXK));
int n,k;
int lowbit(int i)
{
return i&-i;
}
void update(int x,int y,int v)
{
for(int i=x;i<MAXV;i+=lowbit(i))
{
for(int j=y;j<=k+1;j+=lowbit(j))
{
tree[i][j]=max(tree[i][j],v);
}
}
}
int query(int x,int y)
{
int ans=0;
for(int i=x;i>0;i-=lowbit(i))
{
for(int j=y;j>0;j-=lowbit(j))
{
ans=max(ans,tree[i][j]);
}
}
return ans;
}
void solve()
{
cin>>n>>k;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//因为是最长不下降子序列,所以在执行区间加的操作时
//若从某个位置开始增加,那么必然需要一直增加到结尾
//所以,右侧数的+1次数必然大于等于左侧数的+1次数
//考虑定义dp[v][j]为已经执行过j次操作,以数值v结尾的最长不下降子序列长度
//所以若当前数值为v,那么就能更新j=0~k,dp[v+j][j]的格子
//那么就有转移dp[v][j]=1+max(x=1~v,p=0~j)dp[x][p])
//所以这个枚举过程可以用二维树状数组优化
//注意要把j偏移1个位置
for(int i=1;i<=n;i++)
{
//倒序枚举 -> 避免自己产生干扰
for(int j=k;j>=0;j--)
{
int v=a[i]+j;
int dp=query(v,j+1)+1;
update(v,j+1,dp);
}
}
cout<<query(MAXV-1,k+1)<<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;
}
首先,稍微感觉一下(bushi)就能发现,因为是最长不下降子序列,所以在执行区间加的操作时,若要从某个位置开始增加,那么必然需要一直增加到结尾,否则就会导致求最长不下降子序列的这个问题变得更困难。所以,右侧数的 +1 次数必然大于等于左侧数的 +1 次数。
之后,考虑定义 dp[v][j] 为已经执行过 j 次操作,以数值 v 结尾的最长不下降子序列长度。所以若当前数为 v,那么就能更新 j=0~k,dp[v+j][j] 的格子。所以就依赖 1+max(x=1~v,p=0~j)dp[x][p]),这个枚举过程可以用二维树状数组优化,注意要把 j 偏移 1 个位置。那么最后只需要返回以所有数值 v 结尾,用了 0~k 次的最大值即可。
需要注意的是,在每次枚举操作次数 j 的时候,需要倒序枚举,否则之前更新过的值会产生干扰。
四、最长理想子序列
cpp
typedef long long ll;
template<typename T>
struct Segment_Tree
{
int n;
//自定义
vector<T>tree;
Segment_Tree(int _n){
n=_n-1;
tree.resize(_n<<2);
}
//初始全0不用build
void build(const vector<T>&a,int l,int r,int i)
{
if(l==r){
//自定义
tree[i]=a[l];
}
else{
int m=(l+r)>>1;
build(a,l,m,i<<1);
build(a,m+1,r,i<<1|1);
up(i);
}
//自定义
}
void add(int jobl,int jobr,T jobv,int l,int r,int i){
if(jobl<=l&&r<=jobr){
addLazy();
}
else{
int m=(l+r)>>1;
down();
if(jobl<=m){
add(jobl,jobr,jobv,l,m,i<<1);
}
if(m+1<=jobr){
add(jobl,jobr,jobv,m+1,r,i<<1|1);
}
up(i);
}
}
void change(int jobl,int jobr,T jobv,int l,int r,int i){
if(jobl<=l&&r<=jobr){
updateLazy(jobv,i);
}
else{
int m=(l+r)>>1;
down();
if(jobl<=m){
change(jobl,jobr,jobv,l,m,i<<1);
}
if(m+1<=jobr){
change(jobl,jobr,jobv,m+1,r,i<<1|1);
}
up(i);
}
}
T query(int jobl,int jobr,int l,int r,int i){
if(jobl<=l&&r<=jobr){
return tree[i];
}
int m=(l+r)>>1;
down();
//自定义
T ans=0;
if(jobl<=m){
ans=max(ans,query(jobl,jobr,l,m,i<<1));
}
if(m+1<=jobr){
ans=max(ans,query(jobl,jobr,m+1,r,i<<1|1));
}
return ans;
}
//自定义
void addLazy(){
}
//自定义
void updateLazy(T v,int i){
tree[i]=v;
}
//自定义
void up(int i){
tree[i]=max(tree[i<<1],tree[i<<1|1]);
}
//自定义
void down(){
}
};
class Solution {
public:
int longestIdealString(string s, int k) {
int n=s.length();
s=" "+s;
//定义dp[v]为当前以v结尾的最长合法子序列的长度
//那么对于当前字符v,合法的转移就是dp[v-k~v+k]
//所以可以考虑用线段树维护,那么即使k很大也是ok的了
int ans=0;
Segment_Tree<int>tree(26+1);
for(int i=1;i<=n;i++)
{
int v=s[i]-'a'+1;
int l=max(v-k,1);
int r=min(v+k,26);
int dp=tree.query(l,r,1,26,1)+1;
ans=max(ans,dp);
tree.change(v,v,dp,1,26,1);
}
return ans;
}
};
首先,这个状态肯定是要既包含当前位置和最后一个字符,也就是 dp[i][v] 的形式。又可以发现,在递推的过程中其实不关心是从哪个位置转移过来的。那么就可以省略第一维,直接定义 dp[v] 为当前以 v 结尾的最长合法子序列的长度。所以对于当前字符 v,合法的转移就是 dp[v-k~v+k] 中的最大值。那么对于区间查询和单点修改操作,就可以考虑用线段树维护了,那么即使 k 很大也是可以的了。
五、B. The Bakery
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 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};
template<typename T>
struct Segment_Tree
{
vector<T>tree;
//自定义
vector<T>info;
Segment_Tree(int n){
tree.resize(n<<2);
info.resize(n<<2);
}
void clear(int l,int r,int i)
{
if(l==r){
tree[i]=0;
}
else{
int m=(l+r)>>1;
clear(l,m,i<<1);
clear(m+1,r,i<<1|1);
}
//自定义
info[i]=0;
}
//初始全0不用build
void build(int l,int r,int i,const vector<T>&a)
{
if(l==r){
//自定义
tree[i]=a[l];
}
else{
int m=(l+r)>>1;
build(l,m,i<<1,a);
build(m+1,r,i<<1|1,a);
up(i);
}
//自定义
info[i]=0;
}
void add(int jobl,int jobr,T jobv,int l,int r,int i){
if(jobl<=l&&r<=jobr){
addLazy(i,jobv);
}
else{
int m=(l+r)>>1;
down(i);
if(jobl<=m){
add(jobl,jobr,jobv,l,m,i<<1);
}
if(m+1<=jobr){
add(jobl,jobr,jobv,m+1,r,i<<1|1);
}
up(i);
}
}
// void change(int jobl,int jobr,T jobv,int l,int r,int i){
// if(jobl<=l&&r<=jobr){
// updateLazy();
// }
// else{
// int m=(l+r)>>1;
// down();
// if(jobl<=m){
// change(jobl,jobr,jobv,l,m,i<<1);
// }
// if(m+1<=jobr){
// change(jobl,jobr,jobv,m+1,r,i<<1|1);
// }
// up();
// }
// }
T query(int jobl,int jobr,int l,int r,int i){
if(jobl<=l&&r<=jobr){
return tree[i];
}
int m=(l+r)>>1;
down(i);
//自定义
T ans=0;
if(jobl<=m){
ans=max(ans,query(jobl,jobr,l,m,i<<1));
}
if(m+1<=jobr){
ans=max(ans,query(jobl,jobr,m+1,r,i<<1|1));
}
return ans;
}
//自定义
void addLazy(int i,T v){
tree[i]+=v;
info[i]+=v;
}
//自定义
void updateLazy(){
}
//自定义
void up(int i){
tree[i]=max(tree[i<<1],tree[i<<1|1]);
}
//自定义
void down(int i){
if(info[i])
{
addLazy(i<<1,info[i]);
addLazy(i<<1|1,info[i]);
info[i]=0;
}
}
};
void solve()
{
int n,k;
cin>>n>>k;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
//定义dp[p][i]为1~i范围上,最多分成p段的最大得分
//当i<p时,数字个数小于段数,那么就直接等于dp[p-1][i]
//否则,就枚举最后一段的范围,即依赖max{dp[p-1][ii=0~i-1]+ii~i的种类}
//考虑优化枚举行为,假如数组为[a a a b b b a]
//对于dp[p][i],其依赖dp[p-1][0~i-1]
//首先,dp[p-1][0]必然等于0
//之后,dp[p][1]由于1位置必然新增一个字符,所以需要在dp[p-1][0]的基础上+1
//在往后的过程中,由于1~i范围必然有1位置字符,所以这个贡献可以一直推下去
//之后,dp[p][2]由于2位置和1位置的字符相同,所以dp[p-1][0]不会增加1
//而因为2位置新增一个字符,所以dp[p-1][1]要+1
//类似地,dp[p][3]也只在dp[p-1][2]处+1
//之后,因为4位置的字符和前面的字符都不同,所以dp[p-1][0~3]都需要+1
//即在上一次b字符出现的位置~当前位置-1范围上都需要+1
//所以范围增加和范围查询可以用线段树维护
Segment_Tree<int>tree(n+1);
//滚动更新
vector<int>dp(n+1);
for(int p=1;p<=k;p++)
{
//注意范围0~n
tree.build(0,n,1,dp);
//每个数上次出现的位置
vector<int>pre(n+1);
for(int i=1;i<=n;i++)
{
tree.add(pre[a[i]],i-1,1,0,n,1);
if(i>=p)
{
dp[i]=tree.query(0,i-1,0,n,1);
}
pre[a[i]]=i;
}
}
cout<<dp[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[p][i] 为 1~i 范围上,最多分成 p 段的最大得分。那么首先,当 i<p,即数字个数小于段数时,那么就直接等于 dp[p-1][i]。否则,就枚举最后一段的范围,即依赖 max{dp[p-1][j=0~i-1]+j~i的种类}。
之后,考虑优化这个枚举行为,假如数组为 [a a a b b b a],对于 dp[p][i],其依赖 dp[p-1][0~i-1]。首先,dp[p-1][0] 必然等于 0。再之后,dp[p][1] 由于 1 位置必然新增一个字符,所以需要在 dp[p-1][0] 的基础上加一。在往后的过程中,由于 1~i 范围必然有 1 位置字符,所以这个贡献可以一直推下去。之后,dp[p][2] 由于 2 位置和 1 位置的字符相同,所以 dp[p-1][0] 不会增加 1。而因为 2 位置新增一个字符,所以 dp[p-1][1] 要加一。类似地,dp[p][3] 也只在 dp[p-1][2] 处加一。再往后,因为 4 位置的字符和前面的字符都不同,所以 dp[p-1][0~3] 都需要加一,即在上一次 b 字符出现的位置~当前位置 -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 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};
template<typename T>
struct Segment_Tree
{
vector<T>tree;
//自定义
vector<T>info;
Segment_Tree(int n){
tree.resize(n<<2);
info.resize(n<<2);
}
//初始全0不用build
void build(int l,int r,int i,const vector<T>&a)
{
if(l==r){
//自定义
tree[i]=a[l];
}
else{
int m=(l+r)>>1;
build(l,m,i<<1,a);
build(m+1,r,i<<1|1,a);
up(i);
}
//自定义
info[i]=0;
}
void add(int jobl,int jobr,T jobv,int l,int r,int i){
if(jobl<=l&&r<=jobr){
addLazy(i,jobv);
}
else{
int m=(l+r)>>1;
down(i);
if(jobl<=m){
add(jobl,jobr,jobv,l,m,i<<1);
}
if(m+1<=jobr){
add(jobl,jobr,jobv,m+1,r,i<<1|1);
}
up(i);
}
}
// void change(int jobl,int jobr,T jobv,int l,int r,int i){
// if(jobl<=l&&r<=jobr){
// updateLazy();
// }
// else{
// int m=(l+r)>>1;
// down();
// if(jobl<=m){
// change(jobl,jobr,jobv,l,m,i<<1);
// }
// if(m+1<=jobr){
// change(jobl,jobr,jobv,m+1,r,i<<1|1);
// }
// up();
// }
// }
T query(int jobl,int jobr,int l,int r,int i){
if(jobl<=l&&r<=jobr){
return tree[i];
}
int m=(l+r)>>1;
down(i);
//自定义
T ans=INFLL;
if(jobl<=m){
ans=min(ans,query(jobl,jobr,l,m,i<<1));
}
if(m+1<=jobr){
ans=min(ans,query(jobl,jobr,m+1,r,i<<1|1));
}
return ans;
}
//自定义
void addLazy(int i,T v){
tree[i]+=v;
info[i]+=v;
}
//自定义
void updateLazy(){
}
//自定义
void up(int i){
tree[i]=min(tree[i<<1],tree[i<<1|1]);
}
//自定义
void down(int i){
if(info[i])
{
addLazy(i<<1,info[i]);
addLazy(i<<1|1,info[i]);
info[i]=0;
}
}
};
const int MAXN=2e4+5;
int n,k;
vector<ll>dis(MAXN);
vector<ll>cost(MAXN);
vector<ll>range(MAXN);
vector<ll>fix(MAXN);
vector<int>L(MAXN);
vector<int>R(MAXN);
vector<vector<int>>alert(MAXN);
//找大于等于v的最左位置
int bs1(ll v)
{
int l=1;
int r=n;
int m;
int ans=0;
while(l<=r)
{
m=l+r>>1;
if(dis[m]>=v)
{
ans=m;
r=m-1;
}
else
{
l=m+1;
}
}
return ans;
}
//找小于等于v的最右位置
int bs2(ll v)
{
int l=1;
int r=n;
int m;
int ans=0;
while(l<=r)
{
m=l+r>>1;
if(dis[m]<=v)
{
ans=m;
l=m+1;
}
else
{
r=m-1;
}
}
return ans;
}
void build()
{
for(int i=1;i<=n;i++)
{
L[i]=bs1(dis[i]-range[i]);
R[i]=bs2(dis[i]+range[i]);
alert[R[i]].push_back(i);
}
}
vector<ll>dp(MAXN);
void solve()
{
cin>>n>>k;
for(int i=2;i<=n;i++)
{
cin>>dis[i];
}
for(int i=1;i<=n;i++)
{
cin>>cost[i];
}
for(int i=1;i<=n;i++)
{
cin>>range[i];
}
for(int i=1;i<=n;i++)
{
cin>>fix[i];
}
//定义dp[t][i]为最多建t个基站,且最右基站的位置在i号村,1~i号村的最少花费
//存在单调性,随着最多建造个数t的增加,dp值单调不减
//考虑在最后补一个村庄,补一个基站,那么最后的答案就是dp[k+1][n+1]
//根据定义,dp[t][i]肯定可以从dp[t-1][i]转移过来
//之后枚举倒数第二个基站建在哪,那么就是min(dp[t-1][1~i-1]+fix)+cost[i]
//考虑优化枚举
//定义left[i]为最左在几号村庄建基站,i号村庄还能有信号
//定义right[i]为最右在几号村庄建基站,i号村庄还能有信号
//定义alert[i]为i号村庄的预警列表
//表示只有一个基站在i号村,如果基站移动到i+1号村,有哪些村庄会从有信号变成无信号
//在从dp[t][i]到dp[t][i+1]转移时,就需要用到alert[i]
//若alert[i]中有a,若left[a]=p
//那么在转移时,dp[t-1][1~p-1]都需要加上fix[a]
//总时间复杂度O(k*n*logn)
//补一个
dis[++n]=INFLL;
cost[n]=range[n]=fix[n]=0;
//预处理
build();
//因为在补的村庄上一定建基站,所以t=1等效t=0
//初始化t=1
ll sum=0;
for(int i=1;i<=n;i++)
{
dp[i]=sum+cost[i];
for(auto p:alert[i])
{
sum+=fix[p];
}
}
Segment_Tree<ll>tree(n+1);
//dp
for(int t=2;t<=k+1;t++)
{
tree.build(1,n,1,dp);
for(int i=1;i<=n;i++)
{
if(i>=t)
{
dp[i]=min(dp[i],tree.query(1,i-1,1,n,1)+cost[i]);
}
for(auto p:alert[i])
{
if(L[p]>1)
{
tree.add(1,L[p]-1,fix[p],1,n,1);
}
}
}
}
cout<<dp[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[t][i] 为最多建 t 个基站,且最右基站的位置在 i 号村,1~i 号村的最少花费。首先,为了方便计算答案,可以考虑在最后补一个村庄补一个基站,那么最后的答案就是 dp[k+1][n+1]。之后可以发现,存在单调性:随着最多建造个数 t 的增加,dp 值是单调不减的。
根据定义,因为是最多,所以 dp[t][i] 肯定可以从 dp[t-1][i] 转移过来。之后,考虑枚举倒数第二个基站建在哪,那么就是 min(dp[t-1][1~i-1]+fix)+cost[i],那么就需要把这个枚举优化掉。
定义 left[i] 为最左在几号村庄建基站,i 号村庄还能有信号,right[i] 为最右在几号村庄建基站,i 号村庄还能有信号,这两个是可以通过二分来解决的。之后,再定义 alert[i] 为 i 号村庄的预警列表,表示只有一个基站在 i 号村时,如果基站移动到 i+1 号村,有哪些村庄会从有信号变成无信号,那么每次就是往 alert[right[i]] 中加入 i 号村庄。
那么在从 dp[t][i] 到 dp[t][i+1] 转移时,就可以用到 alert[i] 了。若 alert[i] 中有 a,若 left[a]=p,那么在转移时,dp[t-1][1~p-1] 在转移时就都需要加上 fix[a]。所以这个范围增加操作就可以用线段树维护了,时间复杂度O(k*n*logn)。
总结
还是要多练 dp!!