ABC231 倍增记搜 二位数点

D

图论,思维

给一些边,至少要有这些边,其他边可以随便加,问能不能得到一条链。

首先所有点度数不能超过2,因为链上点度数只会为1或2。但这样还可能出现环,环上所有点度数也都是2,没超过2。所以再来个dfs,用搜索树的方式,对所有联通块判环,具体来说就是把每个连通块都当成树,如果搜索时有返祖边就是带环。如果最后没有环就是对的

void solve(void){
	int n,m;
	cin>>n>>m;
	
	vi cnt(n+1);
	vvi g(n+1);
	rep(i,1,m){
		int u,v;
		cin>>u>>v;
		cnt[u]++;
		cnt[v]++;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	
	rep(i,1,n){
		if(cnt[i]>2){
			cout<<"No";
			return;
		}
	}
	vi vis(n+1);
	auto &&dfs=[&](auto &&dfs,int u,int f)->bool{
		vis[u]=1;
		for(int v:g[u]){
			if(v==f)continue;
			if(vis[v]){
				return 1;
			}
			if(dfs(dfs,v,u)){
				return 1;
			}
		}
		return 0;
	};
	rep(i,1,n){
		if(!vis[i]){
			if(dfs(dfs,i,-1)){
				cout<<"No";
				return;
			}
		}
	}
	cout<<"Yes";
}

E

倍增思想,记搜

给一堆硬币,从小到大,每两个相邻的面额都可以是倍数关系。问买 x x x最少需要用多少个硬币,可以找零,但找零使用的硬币也计入。

首先这种一堆互为倍数的面额,显然可以凑出来任意金额,并且想让使用的面额最小,应从大到小使用面额,这类似于倍增lca往上跳的过程。如果不能找零,就每次都选不超过当前金额的最大的即可。

但是这里可以找零,所以每一步就有两种选择,一种是不找零,使用当前的最大的凑出不超过 x x x的,然后还剩下 x m o d    a i x\mod a_i xmodai的钱,递归往下走,用更小的接着凑。也可以用当前面额凑出来刚好超过 x x x的,多出来的钱用更小的面额找零。这里可以注意到找零的过程,对答案的贡献也是一样的,都是使用硬币的个数,所以往下递归不用区分是买还是找零,拥有相同的子问题定义。

这样看起来有个问题:买和找零可能会多次变换,比如第一层是买,第二层是找零,第三层不知道前面是找零的,以为自己是买,也选择找零,又一次转换就又变成买了。换一种说法,最后的方案中 c 1 a 1 + c 2 a 2 + . . . + c n a n c_1a_1+c_2a_2+...+c_na_n c1a1+c2a2+...+cnan c i c_i ci是使用的个数, a i a_i ai是面额, c c c这个序列的正负号可能多次变换,比如 1 , − 1 , − 1 , 1 , − 1 , − 1 1,-1,-1,1,-1,-1 1,1,1,1,1,1,这对吗?

这其实是对的,这相当于所有系数是正的面额都用来买,系数是负的面额都用来找零。

最后就是个每次有两个分支的dfs,需要记忆化。

void solve(void){
	int n,x;
	cin>>n>>x;
	vi a(n+1);
	rep(i,1,n){
		cin>>a[n-i+1];
	}	
	map<int,map<int,int>>dp;
	auto &&dfs=[&](auto &&dfs,int x,int i)->int{
		if(i==n+1){
			return x;
		}	
		if(dp[x][i]){
			return dp[x][i];
		}
		int res=1e18;
		res=min(res,x/a[i]+dfs(dfs,x%a[i],i+1));
		res=min(res,x/a[i]+1+dfs(dfs,a[i]*(x/a[i]+1)-x,i+1));
		
		return dp[x][i]=res;
	};
	cout<<dfs(dfs,x,1);
}

F

二位数点,排序

每种礼物在ab两个人眼里有不同的价值,现在要从n种礼物中给ab两个人送礼,两个人送的可以一样也可以不一样。但如果a眼中认为b得到的礼物比他的更宝贵,就会打架,对b来说也是同理。问不打架的方案数

设a眼中的价值为 a i a_i ai,b眼中价值为 b i b_i bi,这可以转化为,如果送a的是第 i i i个,那么求 a j < = a i , b j > = b i a_j<=a_i,b_j>=b_i aj<=ai,bj>=bi j j j的个数?

很显然的二维数点,先对一个维度排序,满足这个维度的要求,然后用支持区间查的数据结构维护另一个维度,每次查询更大的元素个数。

有一些细节,首先是 a i a_i ai相等 b i b_i bi不相等的情况,比如 ( 2 , 5 ) ( 2 , 10 ) (2,5)(2,10) (2,5)(2,10),显然是可以构成一组答案的,但只有在访问顺序是 ( 2 , 10 ) , ( 2 , 5 ) (2,10),(2,5) (2,10),(2,5)的时候才能计入答案,因此我们需要对第一维度升序,第二维度降序。

其次对于完全相同的两个元素,他们可以构成两组方案,但一般的扫描线只会只会计算一次,因此需要手动把另一半补上。举例来说有序号为 1 , 2 , 3 1,2,3 1,2,3三个完全相同的礼物,扫描线只会计算上 ( 1 , 1 ) ( 1 , 2 ) ( 1 , 3 ) ( 2 , 2 ) ( 2 , 3 ) ( 3 , 3 ) (1,1)(1,2)(1,3)(2,2)(2,3)(3,3) (1,1)(1,2)(1,3)(2,2)(2,3)(3,3)这几个方案,但实际上 ( 2 , 1 ) ( 3 , 1 ) ( 3 , 2 ) (2,1)(3,1)(3,2) (2,1)(3,1)(3,2)也是合法的,假设有 n n n个相同的,有 n ( n − 1 ) 2 \frac{n(n-1)}{2} 2n(n1)个方案被漏算了,把他们手动补上即可

值域很大,需要离散化,这里使用set排序+挨个赋值的做法

struct Tree{
	#define ls u<<1
	#define rs u<<1|1
    struct Node{
        int l,r;
        ll mx,add;
    }tr[N<<2];
     
    void pushup(int u){
        tr[u].mx=tr[ls].mx+tr[rs].mx;
    }
     
    void pushdown(int u){
        if(tr[u].add){
            tr[ls].mx+=tr[u].add*(tr[ls].r-tr[ls].l+1);
            tr[rs].mx+=tr[u].add*(tr[rs].r-tr[rs].l+1);
            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=0;
            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*(tr[u].r-tr[u].l+1);
            tr[u].add+=val;
            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(l<=tr[u].l&&tr[u].r<=r)    return  tr[u].mx;
        pushdown(u);
        int mid=(tr[u].l+tr[u].r)>>1;
        if(r<=mid)return query(ls,l,r);
        if(l>mid)return query(rs,l,r);
        return query(ls,l,r)+query(rs,l, r);
    }
}t;
void solve(void){
	int n;
	cin>>n;
	vi a(n+1),b(n+1);
	vvi c;
	rep(i,1,n){
		cin>>a[i];
	}
	rep(i,1,n){
		cin>>b[i];
	}
	set<int>s;
	map<int,int>mp;
	int cur=0;
	rep(i,1,n){
		s.insert(a[i]);
	}
	for(int x:s){
		mp[x]=++cur;
	}
	rep(i,1,n){
		a[i]=mp[a[i]];
	}
	
	s.clear();
	mp.clear();
	cur=0;
	rep(i,1,n){
		s.insert(b[i]);
	}
	for(int x:s){
		mp[x]=++cur;
	}
	rep(i,1,n){
		b[i]=mp[b[i]];
	}
	map<pii,int>mp1;
	rep(i,1,n){
		mp1[{a[i],b[i]}]++;
		c.push_back({a[i],b[i]});
	}
	
	sort(c.begin(),c.end(),[](vi &a,vi &b){
		if(a[0]==b[0])return a[1]>b[1];
		return a[0]<b[0];
	});
	
	int ans=0;
	for(auto &[k,v]:mp1){
		ans+=(v-1)*v/2;	
	}
	
	int mx=s.size();
	t.build(1,1,mx);
	
	rep(i,0,n-1){
		int x=c[i][1];
		t.modify(1,x,x,1);
		ans+=t.query(1,x,mx);
	}
	cout<<ans;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值