前言
倍增真的很强!!
一、最多可以参加的会议数目 II
cpp
class Solution {
public:
int maxValue(vector<vector<int>>& events, int k) {
int n=events.size();
//考虑对会议按结束时间从小到大排序
//定义dp[i][j]为前i个会议中最多参加j个的最大收益
//对于每个会议,可以选择参加或不参加
//如果要参加的话,可以从前面结束时间小于当前开始时间的会议转移过来
//又因为存在单调性:dp[i][j]<=dp[i+1][j]
//所以可以二分找结束时间小于当前开始时间的最右位置
sort(events.begin(),events.end(),[&](vector<int>&x,vector<int>&y)
{
return x[1]<y[1];
});
vector<vector<int>>dp(n,vector<int>(k+1));
for(int j=1;j<=k;j++)
{
dp[0][j]=events[0][2];
}
for(int i=1;i<n;i++)
{
int pre=bs(events[i][0],i-1,events);
for(int j=1;j<=k;j++)
{
dp[i][j]=max(dp[i-1][j],events[i][2]+(pre==-1?0:dp[pre][j-1]));
}
}
return dp[n-1][k];
}
int bs(int v,int r,vector<vector<int>>&a)
{
int l=0;
int m;
int ans=-1;
while(l<=r)
{
m=l+r>>1;
if(a[m][1]<v)
{
ans=m;
l=m+1;
}
else
{
r=m-1;
}
}
return ans;
}
};
其实这个题和倍增没啥关系()
对于这种开会这种区间问题,还是上来就考虑按左端点或右端点排序。那么在这个题里,就考虑对会议按结束时间从小到大排序,然后定义 dp[i][j] 为前 i 个会议中最多参加 j 个的最大收益。因为对于每个会议,可以选择参加或不参加,所以如果要参加的话,可以从前面结束时间小于当前开始时间的会议转移过来。又因为存在单调性:dp[i][j]<=dp[i+1][j],所以可以二分找结束时间小于当前开始时间的最右位置。
二、跑路
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=50+5;
const int MAXP=64;
//stjump[i][j][p]:从i到j是否有一条长度为2的p次方的路径
bool stjump[MAXN][MAXN][MAXP+1];
//dis[i][j]:从i到j最少需要几秒
int dis[MAXN][MAXN];
int n,m;
void build()
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
stjump[i][j][0]=0;
dis[i][j]=INF;
}
}
}
void solve()
{
cin>>n>>m;
build();
for(int i=1,u,v;i<=m;i++)
{
cin>>u>>v;
stjump[u][v][0]=1;
dis[u][v]=1;
}
//倍增
for(int p=1;p<=MAXP;p++)
{
//Floyd -> 枚举跳点
for(int jump=1;jump<=n;jump++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(stjump[i][jump][p-1]&&stjump[jump][j][p-1])
{
stjump[i][j][p]=1;
dis[i][j]=1;
}
}
}
}
}
//Floyd
for(int jump=1;jump<=n;jump++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(dis[i][jump]!=INF&&dis[jump][j]!=INF)
{
dis[i][j]=min(dis[i][j],dis[i][jump]+dis[jump][j]);
}
}
}
}
cout<<dis[1][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;
}
没想到 floyd 这玩意儿的复杂度居然真能用上()
因为这个折跃门每次是跳2的p次方步的,那么其实很自然就能想到用倍增去维护。首先,可以考虑求出在一秒内从每个点能到哪些点,那么这个就可以直接倍增。首先,考虑定义 stjump[u][v][p] 为从 u 到 v 是否存在一条长度为 2 的 p 次方的路径,再定义 dis[u][v] 表示从 u 到 v 最少需要花几秒。那么对于每一条边 (u,v),必然会带来一条长度为 2 的 0 次方的边,那么就设置 st 表并设置 dis[u][v] 为 1。之后,考虑倍增更新 st 表和 dis 表。此时在枚举每个 p 时,因为点的个数很少,所以这里就可以考虑使用 floyd 算法。那么就是三重循环枚举,每次若从 i 到跳点和跳点到 j 都存在一条长度为 2 的 p-1 次方长度的路径,那么 i 到 j 就存在一条长度为 2 的 p 次方的路径,同时更新 dis[i][j]。最后再跑一遍 floyd 算法,更新 dis 即可。
注意,虽然点的个数很小,但因为每次是跳 2 的 p 次方,那么其实是可以通过在环上绕非常多圈从而一步跳到想去的位置的,所以最终路径长度也可能会很长,所以需要把 MAXP 开大点。
三、统计重复个数
cpp
class Solution {
public:
typedef long long ll;
int getMaxRepetitions(string s1, int a, string s2, int b) {
int n=s1.length();
int m=s2.length();
//next[i][j]:从s1的i位置出发,到下一个j字符的距离
vector<vector<int>>next(n,vector<int>(26));
//s2中存在某个字符在s1中没出现过
if(!find(s1,n,s2,next))
{
return 0;
}
//st[i][p]:从s1的i位置出发,走多远能获得2^p个s2
vector<vector<ll>>st(n,vector<ll>(30+1));
//构建st[i][0]
for(int i=0;i<n;i++)
{
int cur=i;
ll len=0;
for(auto c:s2)
{
len+=next[cur][c-'a'];
cur=(cur+next[cur][c-'a'])%n;
}
st[i][0]=len;
}
//倍增
for(int p=1;p<=30;p++)
{
for(int i=0;i<n;i++)
{
st[i][p]=st[i][p-1]+st[(i+st[i][p-1])%n][p-1];
}
}
//a个s1中能拼出几个s2
ll ans=0;
for(int p=30,cur=0;p>=0;p--)
{
//从当前位置cur开始,是否能拼出2^p个s2
if(cur+st[cur%n][p]<=n*a)
{
cur+=st[cur%n][p];
ans+=1ll<<p;
}
}
//用a个s1能拼出ans个s2,s2拼接了b次
//所以需要再拼接ans/b次即可
return ans/b;
}
bool find(string &s1,int n,string &s2,vector<vector<int>>&next)
{
vector<int>right(26,-1);
for(int i=n-1;i>=0;i--)
{
right[s1[i]-'a']=i+n;
}
for(int i=n-1;i>=0;i--)
{
right[s1[i]-'a']=i;
for(int j=0;j<26;j++)
{
if(right[j]!=-1)
{
next[i][j]=right[j]-i+1;
}
else
{
next[i][j]=-1;
}
}
}
for(auto c:s2)
{
//s1中不存在s2中的这个字符
if(next[0][c-'a']==-1)
{
return false;
}
}
return true;
}
};
首先,因为 n1,n2 都很大,那么复杂度基本上肯定不能和这两个量有关了。那么在拼接的过程中,可以考虑用倍增处理能拼出几个的问题。那么就可以考虑定义 st[i][p] 为从s1 的 i 位置出发,走多远能获得 2 的 p 次方个 s2。那么首先,st[i][0] 的含义是从 i 位置出发走多远能获得 1 个s2。那么在拼 s2 的过程中,由于每次都要找到从当前位置开始下一个某字符的位置,所以考虑通过预处理一个结构解决。那么考虑定义 next[i][j] 为从 s1 的 i 位置开始,到下一个 j 字符的距离。这个只需要维护一个 right 表示上一个 j 字符出现的位置,那么每次往 next 里刷即可。那么首先就可以进行特判,若 s1 中不存在 s2 的某个字符,那么就必然不可能拼出来。
之后,在构建 st[i][0] 拼 s2 的过程中,可以每次用 next 数组往下跳,每次取个模即可。那么有了 st[i][0],就可以开始倍增了。最后只需要考虑 a 个 s1 能拼出几个 s2,那么直接通过倍增往下跳,最后把能拼出的个数除以 b 就是能拼出几个重复 b 次的 s2。
四、双链表解决最近与次近问题
在看下个题之前,先看以下问题:
给定一个长度为 n 的数组 arr,下标 1~n,数组内无重复值。关于近和距离的定义如下:对 i 位置的数字 x 来说,只关注右侧的数字,和 x 的差值的绝对值越小就越近,距离就是差值的绝对值。如果距离一样,数值越小的越近。求每个数字最近和次近的位置及其距离,如果不存在用 0 表示。
cpp
const int MAXN=1e5+5;
const int LIMIT=20;
vector<int>h(MAXN);
int n;
//从i出发,第一近和第二近的点及距离
vector<int>to1(MAXN);
vector<int>dis1(MAXN);
vector<int>to2(MAXN);
vector<int>dis2(MAXN);
bool cmp(const pii &x,const pii &y)
{
return x.second!=y.second?x.second<y.second:x.first<y.first;
}
void update(int i,int j)
{
if(j==0)
{
return ;
}
int dis=abs(h[i]-h[j]);
if(to1[i]==0||dis<dis1[i]||(dis==dis1[i]&&h[j]<h[to1[i]]))
{
to2[i]=to1[i];
dis2[i]=dis1[i];
to1[i]=j;
dis1[i]=dis;
}
else if(to2[i]==0||dis<dis2[i]||(dis==dis2[i]&&h[j]<h[to2[i]]))
{
to2[i]=j;
dis2[i]=dis;
}
}
//有序表方法
void near1()
{
set<pii,decltype(&cmp)>st(cmp);
//倒序构建
for(int i=n;i>=1;i--)
{
pii cur={i,h[i]};
//小于cur的两条
auto p1=st.lower_bound(cur);
if(p1!=st.begin())
{
p1=prev(p1);
update(i,p1->first);
if(p1!=st.begin())
{
auto p2=prev(p1);
update(i,p2->first);
}
}
//大于cur的两条
auto p3=st.upper_bound(cur);
if(p3!=st.end())
{
update(i,p3->first);
auto p4=next(p3);
if(p4!=st.end())
{
update(i,p4->first);
}
}
st.insert(cur);
}
}
vector<array<int,2>>sorted(MAXN);
vector<int>pre(MAXN);
vector<int>nxt(MAXN);
void remove(int i)
{
int l=pre[i];
int r=nxt[i];
if(l!=0)
{
nxt[l]=r;
}
if(r!=0)
{
pre[r]=l;
}
}
//双向链表方法
void near2()
{
for(int i=1;i<=n;i++)
{
sorted[i][0]=i;
sorted[i][1]=h[i];
}
sort(sorted.begin()+1,sorted.begin()+n+1,[&](auto &x,auto &y)
{
return x[1]<y[1];
});
sorted[0][0]=0;
sorted[n+1][0]=0;
//排名
for(int i=1;i<=n;i++)
{
pre[sorted[i][0]]=sorted[i-1][0];
nxt[sorted[i][0]]=sorted[i+1][0];
}
//原数组下标
for(int i=1;i<=n;i++)
{
update(i,pre[i]);
update(i,pre[pre[i]]);
update(i,nxt[i]);
update(i,nxt[nxt[i]]);
remove(i);
}
}
首先说有序表方法,那么就是定义一个 set,根据数值从小到大排序。注意这里写比较器时要考虑数值相同的情况,否则 set 会只保留一个键。之后从后往前考虑,每次查小于当前数值的两条和大于当前数值的两条,比较看能否更新最近和次近,然后把当前值和下标加入 set 即可。
再说双向链表的实现,考虑先把所有值拿出来从小到大排序,然后根据排名去生成双向链表。最后只需要从左到右考察整个数组,通过这个双向链表每次考察左侧的两个位置和右侧的两个位置,此时就是右侧大于自己和小于自己的两条数据,最后删除自己这个节点,防止干扰后续即可。
五、开车旅行
解决了上面这个问题,就可以正式开始这个题了(晕)
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;
const int LIMIT=20;
vector<int>h(MAXN);
int n;
//从i出发,第一近和第二近的点及距离
vector<int>to1(MAXN);
vector<int>dis1(MAXN);
vector<int>to2(MAXN);
vector<int>dis2(MAXN);
bool cmp(const pii &x,const pii &y)
{
return x.second!=y.second?x.second<y.second:x.first<y.first;
}
void update(int i,int j)
{
if(j==0)
{
return ;
}
int dis=abs(h[i]-h[j]);
if(to1[i]==0||dis<dis1[i]||(dis==dis1[i]&&h[j]<h[to1[i]]))
{
to2[i]=to1[i];
dis2[i]=dis1[i];
to1[i]=j;
dis1[i]=dis;
}
else if(to2[i]==0||dis<dis2[i]||(dis==dis2[i]&&h[j]<h[to2[i]]))
{
to2[i]=j;
dis2[i]=dis;
}
}
//有序表方法
void near1()
{
set<pii,decltype(&cmp)>st(cmp);
//倒序构建
for(int i=n;i>=1;i--)
{
pii cur={i,h[i]};
//小于cur的两条
auto p1=st.lower_bound(cur);
if(p1!=st.begin())
{
p1=prev(p1);
update(i,p1->first);
if(p1!=st.begin())
{
auto p2=prev(p1);
update(i,p2->first);
}
}
//大于cur的两条
auto p3=st.upper_bound(cur);
if(p3!=st.end())
{
update(i,p3->first);
auto p4=next(p3);
if(p4!=st.end())
{
update(i,p4->first);
}
}
st.insert(cur);
}
}
vector<array<int,2>>sorted(MAXN);
vector<int>pre(MAXN);
vector<int>nxt(MAXN);
void remove(int i)
{
int l=pre[i];
int r=nxt[i];
if(l!=0)
{
nxt[l]=r;
}
if(r!=0)
{
pre[r]=l;
}
}
//双向链表方法
void near2()
{
for(int i=1;i<=n;i++)
{
sorted[i][0]=i;
sorted[i][1]=h[i];
}
sort(sorted.begin()+1,sorted.begin()+n+1,[&](auto &x,auto &y)
{
return x[1]<y[1];
});
sorted[0][0]=0;
sorted[n+1][0]=0;
//排名
for(int i=1;i<=n;i++)
{
pre[sorted[i][0]]=sorted[i-1][0];
nxt[sorted[i][0]]=sorted[i+1][0];
}
//原数组下标
for(int i=1;i<=n;i++)
{
update(i,pre[i]);
update(i,pre[pre[i]]);
update(i,nxt[i]);
update(i,nxt[nxt[i]]);
remove(i);
}
}
//a开一次b开一次称为一轮
//stto[i][p]:从i位置出发,经过2^p轮到了什么位置
vector<vector<int>>stto(MAXN,vector<int>(LIMIT));
//stdis[i][p]:从i位置出发,经过2^p轮走了多远
vector<vector<int>>stdis(MAXN,vector<int>(LIMIT));
//sta[i][p]:从i位置出发,经过2^p轮,a走了多远
vector<vector<int>>sta(MAXN,vector<int>(LIMIT));
//stb[i][p]:从i位置出发,经过2^p轮,b走了多远
vector<vector<int>>stb(MAXN,vector<int>(LIMIT));
void build()
{
for(int i=1;i<=n;i++)
{
stto[i][0]=to1[to2[i]];
stdis[i][0]=dis2[i]+dis1[to2[i]];
sta[i][0]=dis2[i];
stb[i][0]=dis1[to2[i]];
}
for(int p=1;p<LIMIT;p++)
{
for(int i=1;i<=n;i++)
{
stto[i][p]=stto[stto[i][p-1]][p-1];
//没开出去
if(stto[i][p]!=0)
{
stdis[i][p]=stdis[i][p-1]+stdis[stto[i][p-1]][p-1];
sta[i][p]=sta[i][p-1]+sta[stto[i][p-1]][p-1];
stb[i][p]=stb[i][p-1]+stb[stto[i][p-1]][p-1];
}
}
}
}
int a,b;
void go(int s,int x)
{
a=0,b=0;
for(int p=LIMIT-1;p>=0;p--)
{
if(stto[s][p]!=0&&x>=stdis[s][p])
{
x-=stdis[s][p];
a+=sta[s][p];
b+=stb[s][p];
s=stto[s][p];
}
}
//a还能多开一次
if(dis2[s]<=x)
{
a+=dis2[s];
}
}
int best(int x0)
{
int ans=0;
double rat=INF;
//从每个点开始跑一遍
for(int i=1;i<n;i++)
{
go(i,x0);
double cur=INF;
if(b!=0)
{
cur=(double)a/b;
}
if(ans==0||cur<rat||(cur==rat&&h[i]>h[ans]))
{
rat=cur;
ans=i;
}
}
return ans;
}
void solve()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>h[i];
}
near2();
build();
int x0;
cin>>x0;
cout<<best(x0)<<endl;
int m;
cin>>m;
int s,x;
while(m--)
{
cin>>s>>x;
go(s,x);
cout<<a<<" "<<b<<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;
}
首先,定义 a 开一次 b 开一次为一轮,之后考虑构建四张倍增表,分别是:stto[i][p] 表示从 i 出发经过 2 的 p 次方轮能到达哪个点,stdis[i][p] 表示从 i 位置出发,经过 2 的 p 次方轮走了多远,sta[i][p] 表示从 i 位置出发,经过 2 的 p 次方轮,a 走了多远,stb[i][p] 表示从 i 位置出发,经过 2 的 p 次方轮,b 走了多远。那么因为有了最近和次近的两张表,这四个表的构建就非常简单了。
构建完倍增表后,每次查询时,只需要根据这个倍增表往后跳,每次记录 a 和 b 分别开了多远即可,注意最后需要特判 a 能否单独开一次。那么在有了这个方法后,只需要枚举从每个位置开始开,每次查询一下取最小值即可。
总结
还是太难......