寒假训练营2
C 字典树
求升序子序列个数,其中最大值和最小值的异或小于等于d。如果确定了最小值最大值分别为a[i],a[j],,那么中间的数都可选可不选,这组maxmin对总方案数的贡献就是2^(j-i-1)。
直接二重循环枚举minmax显然是不行的,只能枚举一个,然后logn查询另一个。但这个查询是要查询抑或上一个数不超过d的元素个数,此时用字典树才能达到logn。
字典树用叶子节点表示一个元素,从根节点到叶结点的路径则是这个元素的二进制表示,0为左,1为右。查询时,由于异或的结果不大于d,如果d某一位是1,那么minmax在这一位可以一样,也可以不一样,如果不一样,接着往下走,直到达到叶节点,更新叶节点代表的数的贡献;如果一样,异或后这一位为0,由于前面位都是d的子集,后面位可以任取了,都一定不大于d,那么就不用往下找了,直接加上这棵子树的贡献。
对于贡献,可以拆开成2^(j-1)/2^i,j是max决定的,查询时只用求sigma(1/2^i),也就是说每个叶节点对答案的贡献是1/2^i,那么我们把1/2^i保存在字典树节点上,查询时累加。
涉及到除法的模运算,用逆元。
为了不漏,先对原数组元素排序,然后从小到大枚举max
ll val;
int son[2];
}t[N*32];
void init(void){
cnt=1;
for(int i=0;i<=n*32;i++){
t[i].val=t[i].son[1]=t[i].son[0]=0;
}
}
void insert(ll val,int x){
int u=1;
for(int i=30;i>=0;i--){
int s=(x>>i)&1;
if(t[u].son[s]==0)t[u].son[s]=++cnt;
u=t[u].son[s];
t[u].val=(t[u].val+val)%mod;
}
}
ll get(int x){
int u=1;
ll ret=0;
for(int i=30;i>=0;i--){
int s=(x>>i)&1;
int m=(k>>i)&1;
if(m){
if(t[u].son[s])ret=(ret+t[t[u].son[s]].val)%mod;
u=t[u].son[s^1];
}
else u=t[u].son[s];
}
ret=(ret+t[u].val)%mod;
return ret;
}
signed main(){
int T;
cin>>T;
while(T--){
cin>>n>>k;
init();
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+1+n);
ll ans=1;
for(int i=2;i<=n;i++){
insert(inv(qpow(2,i-1)),a[i-1]);
ans=(ans+1+get(a[i])*qpow(2,i-1))%mod;
}
cout<<ans<<'\n';
}
}
D 同余最短路
初始在k位,目标是到n位,有m张卡牌可以让位置上升a[i],代价为b[i],可以重复使用,位置大于n时会取模,问到达n的最小代价是多少?
可以看成一个最短路问题。每个位置都有m条出边,终点为从这点出发,用了第i张牌后的位置,边权为这张牌的代价,然后以k为起点,n为终点跑一个最短路就好了,最短路程就是最小代价。
需要注意的是不用建图,找出边时遍历m种卡牌,算出对应终点就行。
{
int to,val,next;
}edge[1000*N];//存边数组初始化为边数大小m
struct node
{
int val,no;
};
bool operator<(node a,node b)
{
return a.val>b.val;//重载运算符
}
priority_queue<node>q;
int n,m,s,a[N],b[N],d[N],cnt,head[N],vis[N];
void add(int u,int v,int w){
edge[++cnt]=(EDGE){v,w,head[u]};
head[u]=cnt;
}
signed main(){
int T;
cin>>T;
while(T--){
cin>>n>>m>>s;
s--;
rep(i,0,n-1)d[i]=1e18;
memset(head,0,sizeof(head));
memset(vis,0,sizeof(vis));
while(q.size())q.pop();
cnt=0;
rep(i,1,m){
cin>>a[i]>>b[i];
}
// rep(i,1,n){
// rep(j,1,m){
// add(i,(i+a[j])%n,b[j]);
// }
// }
d[s]=0;
q.push(node{0,s});
while(!q.empty())
{
int x=q.top().no;//取堆顶编号
q.pop();
if(vis[x])continue;//在a中直接删掉
vis[x]=1;//标记为在a中
rep(i,1,m)//遍历m条出边
{
int to=(x+a[i])%n;
if(d[to]>d[x]+b[i])
{
d[to]=d[x]+b[i];
if(!vis[to])
q.push(node{d[to],to});
}
}
}
if(d[n-1]!=1e18)cout<<d[n-1]<<'\n';
else cout<<-1<<'\n';
}
}
F 思维
每次从各个颜色最右边的宝石宝石中选择一个,消除该位置及其右边所有宝石,问最少需要多少次可以消除所有宝石?
第一问用了个复杂的贪心,只能过第一问。保存每个颜色的宝石的最右边的位置,然后每次取最小值,删除右边,然后更新所有颜色宝石的最右边的位置。这个更新要用Lowerbound,然后最多有n种宝石,每轮更新的复杂度就要到nlogn了。可能更新轮数不会太多,最终复杂度也还能过,但是实现起来太麻烦了,不写了。
一种更简单也更快的贪心是,从后往前遍历,如果所有颜色的宝石都出现过一次了,就把当前位置及右边的宝石都删掉,一位内所有颜色的宝石都出现过了,说明所有颜色的最右侧都已经出现了,而最后出现的,就是所有颜色最右侧点中最靠左的点,从这点开始删除可以删掉最多的宝石,也就可以使删除次数最少。
实现时需要注意的是,需要记录每种颜色的个数,如果某种颜色已经全删掉了,那么就应该把颜色总数减一。
map<int,int>cnt;
map<int,int>vis;
int main(){
int T;
cin>>T;
while(T--){
int n;
cin>>n;
cnt.clear();
vis.clear();
for(int i=1;i<=n;i++){
cin>>a[i];
cnt[a[i]]++;//统计每种颜色个数
}
int vised=0;//出现的颜色数
int ans=0;
int tot=cnt.size();//颜色总数
int up=n;//上次删除点,删除的上界
for(int i=n;i>=1;i--){
if(!vis[a[i]]){//记录每种颜色是否出现
vis[a[i]]=1;
vised++;
}
if(vised==tot){//如果都出现了
vised=0;//重置
ans++;
vis.clear();
for(int j=i;j<=up;j++){
cnt[a[j]]--;//更新每种颜色个数
if(!cnt[a[j]])tot--;//如果有颜色删光了,更新总颜色
}
up=i-1;//更新上界
}
}
cout<<ans<<'\n';
}
}
G线段树/树状数组
对每个查询l,r,找到[l,r]内一个区间[i,j],确定一个x,使得sum[i,x]-sum[x,j]最大。由于所有元素都是非负的,那么首先,i一定等于l,因为如果不等于l,sum[l,x]-sum[x,j]一定比sum[i,x]-sum[x,j]更大,[i,j]和x就不是最大的选法了。其次,对于一个[i,j],x一定等于j,还是因为所有数都是非负的,减数肯定越少越好,被减数肯定越多越好。
那么对于一个查询l,r,我们实际上就是要维护max(sum[l,i] - a[i+1] * 2),其中i<r。
如果使用树状数组,我们可以logn查询sum[l,i],但是求max(sum[l,i] - a[i+1] * 2)还是要遍历[l,r]的,n次查询,lr范围也在1到n,总复杂度为n^2logn,显然是不行的。
这里可以用一个巧妙的贪心,就是每次查询都不用遍历[l,r],只要遍历从r-1往前最多32个位置就可以了。因为对于每次计算的f[i]=sum[l,i] - a[i+1] * 2,i往前挪一位时,f[i-1]=f[i]+a[i] - 2 * a[i-1],只有a[i] - 2 * a[i-1]>0,也就是a[i-1]<a[i]/2时,f[i-1]才会大于f[i]。而元素范围不超过Int(2^31)的话,一个满足a[i-1]<a[i]/2的连续序列最长为32,因此从r-1开始往前遍历,f[i]最多在前32次能变大,那么为了求最大值,我们就只用遍历前面最多32个位置。
这样做复杂度就降低到了32nlogn,是可以接受的。
return x&(-x);
}
void add(int x,int v){
while(x<=n){
b[x]+=v;
x+=lowbit(x);
}
}
ll s(int x){
ll ret=0;
while(x>0){
ret+=b[x];
x-=lowbit(x);
}
return ret;
}
int main(){
int T;
cin>>T;
while(T--){
cin>>n>>q;
for(int i=1;i<=n;i++)b[i]=0;
for(int i=1;i<=n;i++){
cin>>a[i];
add(i,a[i]);//建树
}
for(int i=1;i<=q;i++){
int op,x,y;
cin>>op>>x>>y;
if(op==1){
add(x,y-a[x]);//单点修改
a[x]=y;
}
else{
ll ans=-1e18;
for(int j=y;j>=max(x+1,y-32);j--){//往前最多32个
ans=max(ans,s(j)-s(x-1)-2*a[j]);
}
cout<<ans<<'\n';
}
}
}
}
树状数组只能用这种贪心才能通过,是因为树状数组只支持修改和查询区间和这两种操作,而线段树更加灵活,可以维护区间上更多的量,因此不需要用这种贪心也可以通过。前面是说了,我们需要的就是max(sum[l,i] - a[i+1] * 2),那么我们就可以在线段数上维护这个量。两个区间合并时,新的max可能是左区间的max,右区间的max,或者是左区间全部加上右区间max。
sum是一个前缀和,但是中途可能有修改,因此也需要一个支持log查询修改区间和的结构来做,这里为了方便用一个树状数组。
修改时要对树状数组进行单点修改,也要修改原始数组a(更新线段树时要用到a),还要对线段树进行修改。由于线段树维护的是max(sum[l,i] - a[i+1] * 2),其中sum是前缀和,对原数组进行单点修改会影响后面每一个sum的值,也就会影响后面所有max(sum[l,i] - a[i+1] * 2),修改后需要给线段树上修改位置直到结尾所有点都加上修改值。
ll a[N],b[N];
int lowbit(int x){
return x&(-x);
}
void add(int x,int v){
while(x<=n){
b[x]+=v;
x+=lowbit(x);
}
}
ll s(int x){
ll ret=0;
while(x){
ret+=b[x];
x-=lowbit(x);
}
return ret;
}
struct Tree{
struct Node{
int l,r;
ll mx,add;
}tr[N<<2];
void pushup(int u){
tr[u].mx=max(tr[ls].mx,tr[rs].mx);
}
void pushdown(int u){
if(tr[u].add){
tr[ls].mx+=tr[u].add;//下放,最大值加
tr[rs].mx+=tr[u].add;
tr[ls].add+=tr[u].add;//标记加
tr[rs].add+=tr[u].add;
tr[u].add=0;
}
}
void build(int u,int l,int r){
tr[u]={l,r,0,0};
if(l==r){
tr[u].mx=s(l-1)-a[l];//初始化成要维护的值
return;
}
int mid=(l+r)>>1;
build(ls,l,mid); build(rs,mid+1,r);
pushup(u);
}
void modify(int u,int l,int r,int val){
if(tr[u].l>=l&&tr[u].r<=r){ //区间修改
tr[u].mx+=val;//单点加v,后面sum都加v,因此区间mx加v
tr[u].add+=val;//区间也加v,需要lazytag
return ;
}
else{
int mid=(tr[u].l+tr[u].r)>>1;
pushdown(u);
if(mid>=l) modify(ls,l,r,val);
if(r>mid) modify(rs,l,r,val);
pushup(u);
}
}
ll query(int u,int l,int r){
if(r<tr[u].l||tr[u].r<l) return -1e18;//维护最大值,默认-inf
if(l<=tr[u].l&&tr[u].r<=r) return tr[u].mx;
pushdown(u);
return max(query(ls,l,r),query(rs,l, r));
}
}t;
int main(){
int T;
cin>>T;
while(T--){
cin>>n>>q;
for(int i=1;i<=n;i++)b[i]=0;//树状数组清零
for(int i=1;i<=n;i++){
cin>>a[i];
add(i,a[i]);//加入前缀和树状数组
}
t.build(1, 1, n);//线段树建树就包含清空了
for(int i=1;i<=q;i++){
int op,x,y;
cin>>op>>x>>y;
if(op==1){
t.modify(1, x, x, a[x]-y);
//输入是修改为,板子是加
//x位置维护的是sum[l-1]-a[l],而加是加在a[l]上的
//因此加y-a[x]对x点值的贡献是负的
if(x<n)t.modify(1, x+1, n, y-a[x]);
//对后面的点来说是加在sum[l-1]上的,
//因此贡献是正的
add(x,y-a[x]);//更新前缀和
a[x]=y;//最后再更新原数组,因为上面的增量都要用上一次a[x]算
}
else{
cout<<t.query(1, x+1, y)-s(x-1)<<'\n';
//两个人最少都要有一段因此长度最短为2,[i,j]最小为[l,l+1]
//前缀和需要减去左端点s(l-1)
}
}
}
}