MNNU个人赛1

A

题目大意

\(n×n\) 的网格上放 \(k\) 个勇士,勇士可攻击与它相邻的 8 个格子,问有多少种放置 \(k\) 个勇士的方案使它们之间无法互相攻击。

解题思路

状压 DP

每个格子只有两种状态(放置棋子/不放置棋子),这我们可以用 (1/0)来表示

假设现在有一个 \(3 × 3\) 的网格,问放置一个棋子的方案数

那么所有合法的摆放方案可以用以下状态来表示:

100
000
000
010
000
000
001
000
000
000
100
000
000
010
000
000
001
000
000
000
100
000
000
010
000
000
001

接下来考虑递推公式:

定义 \(cnt[j]\) 表示状态 \(j\) 的 1 的个数

定义 \(dp_{i,j,l}\) 表示第 \(i\) 行状态为 \(j\) , 共放置了 \(l\) 个棋子的方案数

那么可以递推得到 \(dp_{i,j,l} = dp[{i - 1][h][l - cnt_j]}\)

其中 \(h\)\(i - 1\) 行的状态,且 $ j 、h $ 为合法状态 , 且 \(j、h\) 两种状态摆放的棋子不冲突

那么\(ans = ∑dp[n][i][k]\)

AC_Code

#include<bits/stdc++.h>
#define int long long
#define rep(i , a , b) for(int i = a ; i <= b ; i ++)
using namespace std;
const int N = 3e5 + 10;
int n , k , cnt[N] , dp[11][1 << 11][110];
vector<int>vec; // 储存合法状态 
int calc(int x) // 统计二进制中 1 的个数 
{
	int res = 0;
	while(x) 
	{
		if(x & 1) res ++ ;
		x >>= 1;
	}
	return res;
}
bool check(int x) //判断状态 x 是否合法 
{	
	rep(i , 0 , n)
	{
		if((x >> i & 1) && (x >> (i + 1) & 1)) return false;
	}	
	return true;
}
bool ok(int i , int j) // 判断 i , j 俩状态是否冲突 
{
	if((i & j) || (i >> 1 & j) || (j >> 1 & i)) return false;
	return true;
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	cin >> n >> k;
	int sum = (1 << n) - 1;
	rep(i , 0 , sum) if(check(i)) 
	{
			if(cnt[i] > k) continue ; 
			cnt[i] = calc(i);
			vec.push_back(i);
	}	
	for(auto i : vec) dp[1][i][cnt[i]] = 1;
	rep(i , 2 , n) for(auto j : vec)
	for(auto h : vec)
	{
		if(!ok(j , h)) continue ;
		rep(l , 0 , k) 
		{
			if(l < cnt[j]) continue ;
			dp[i][j][l] += dp[i - 1][h][l - cnt[j]];
		}
	}
	int ans = 0;
	for(auto i : vec) ans += dp[n][i][k];
	cout << ans << '\n'; 
	return 0;
}

B

题目大意

有 $ N $ 堆砖头从左向右摆在一条线上。第\(i\)堆砖头所在的坐标为\(Xi\), 它的高度为\(Hi\).
现在可以使用一种炸弹把这些砖头炸碎。在坐标\(X\)上使用 \(1\) 次炸弹会将所有处于 \(X-D\)\(X+D\) (包含端点) 的所有砖头的高度减少 \(A\)
现在希望用尽量少的炸弹来把所有摆出来的砖都炸掉,请计算最少用几次炸弹。

解题思路

\(N\) 堆砖头按照坐标 \(Xi\) 从小到大排序,然后开始贪心
如何贪心呢?

首先观察性质不难发现 , 要将所有砖头的高度炸为 \(0\) , 最左边的砖头高度也必然要炸为 \(0\). 而要将最左边的砖头的高度炸为 \(0\),必然只有一个最优的方案:在 \(X1 + D\) 的位置安放炸弹直到最左边的砖头高度被炸为 \(0\) , 然后再查找下一个最左边的高度不为 \(0\) 的砖头,重复上述操作直到所有砖头高度都为 \(0\)

简略证明一下方案可行性:

定义高度大于 \(0\) 的最左边的砖头坐标为 X
那么要使炸弹可以炸到 \(X\) ,则安置炸弹的范围必须为 \([X-D,X+D]\)
其中将炸弹安置在 \(X+D\) 可以炸到的范围为 \([X,X+2×D]\) , 其余安置点的爆炸范围为 \([Y,Y+2×D]\)\(Y < X\)
因为 \(X\) 的左边已经不存在高度大于 \(0\) 的砖头了,所以其余安置点的有效爆炸范围为 \([X,Y+2×D]\), 小于\([X,X+2×D]\)
所以将炸弹安置在 \(X+D\) 一定最优,贪心方案可行

那么怎么写呢?

通过上述我们可以发现我们需要实现两个操作

1.区间修改 (将区间内每个数的值都减少 A)

2.查询左边第一个下标大于 \(0\) 的砖头

操作 ① 很显然用线段树跑一跑就好了, 操作 ② 也就在线段树上跑个二分就好了

由于 \(X\) 的范围很大线段树存不下,所以需要离散化坐标 \(X\)

AC_Code

#include<bits/stdc++.h>
#define rep(i , a , b) for(int i = a ; i <= b ; i ++)
#define int long long
#define ll long long 
using namespace std;
const ll INF (0x3f3f3f3f3f3f3f3fll);
const int N = 2e5 + 10;
struct Tree
{
	ll l,r,sum,lazy,maxn;
} tree[2000000];
void push_up(ll rt)
{
	tree[rt].sum=tree[rt<<1].sum+tree[rt<<1|1].sum;
	tree[rt].maxn=max(tree[rt<<1].maxn,tree[rt<<1|1].maxn);
}
void push_down(ll rt , ll length)
{
	if(tree[rt].lazy)
	{
		tree[rt<<1].lazy+=tree[rt].lazy;
		tree[rt<<1|1].lazy+=tree[rt].lazy;
		tree[rt<<1].sum+=(length-(length>>1))*tree[rt].lazy;
		tree[rt<<1|1].sum+=(length>>1)*tree[rt].lazy;
		tree[rt<<1].maxn+=tree[rt].lazy;
		tree[rt<<1|1].maxn+=tree[rt].lazy;
		tree[rt].lazy=0;
	}
}
void build(ll l , ll r , ll rt , ll *aa)
{
	tree[rt].lazy=0 , tree[rt].l=l , tree[rt].r=r;
	if(l==r)
	{
		tree[rt].sum=aa[l] , tree[rt].maxn=tree[rt].sum;
		return;
	}
	ll mid=(l+r)>>1;
	build(l,mid,rt<<1,aa);
	build(mid+1,r,rt<<1|1,aa);
	push_up(rt);
}
void update_range(ll L , ll R , ll key , ll rt)
{
	if(tree[rt].r<L||tree[rt].l>R)return;
	if(L<=tree[rt].l&&R>=tree[rt].r)
	{
		tree[rt].sum+=(tree[rt].r-tree[rt].l+1)*key;
		tree[rt].maxn+=key;
		tree[rt].lazy+=key;
		return;
	}
	push_down(rt,tree[rt].r-tree[rt].l+1);
	ll mid=(tree[rt].r+tree[rt].l)>>1;
	if(L<=mid)update_range(L,R,key,rt << 1);
	if(R>mid)update_range(L,R,key,rt << 1 | 1);
	push_up(rt);
}
ll query_range(ll L, ll R, ll rt)
{
	if(L<=tree[rt].l&&R>=tree[rt].r) return tree[rt].sum;
	push_down(rt,tree[rt].r-tree[rt].l+1);
	ll mid=(tree[rt].r+tree[rt].l)>>1;
	ll ans=0;
	if(L<=mid)ans+=query_range(L,R,rt << 1);
	if(R>mid)ans+=query_range(L,R,rt << 1 | 1);
	return ans;
}
ll query_max(ll L, ll R, ll rt)
{
	if(L<=tree[rt].l&&R>=tree[rt].r) return tree[rt].maxn;
	push_down(rt,tree[rt].r-tree[rt].l+1);
	ll mid=(tree[rt].r+tree[rt].l)>>1;
	ll ans=-(0x3f3f3f3f3f3f3f3fll);
	if(L<=mid)ans=max(ans,query_max(L,R,rt << 1));
	if(R>mid)ans=max(ans,query_max(L,R,rt << 1 | 1));
	return ans;
}
ll query_range_size(ll L , ll R , ll rt , ll val)
{
	if(tree[rt].l == tree[rt].r)
		return tree[rt].l;
	push_down(rt,tree[rt].r-tree[rt].l+1);
	ll mid = tree[rt].l + tree[rt].r >> 1;
	if(tree[rt << 1].maxn > val) return query_range_size(L , R , rt << 1 , val);
	else if(tree[rt << 1 | 1].maxn > val) return query_range_size(L , R , rt << 1 | 1 , val);
	else return -1;
}
struct node
{
	int x , h;
	bool operator < (const node & b) const {
		return x < b.x;
	} 
} a[N];
ll n , d , c , ans , ha[N] , dist[N];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	cin >> n >> d >> c;
	rep(i , 1 , n)
	{
		cin >> a[i].x >> a[i].h;
		ha[i] = a[i].h;
	}
	sort(a + 1 , a + 1 + n);
	rep(i , 1 , n) ha[i] = a[i].h , dist[i] = a[i].x;
	ha[n + 1] = INF , dist[n + 1] = INF , a[n + 1].x = INF;
	build(1 , n , 1 , ha);
	rep(i , 1 , n)
	{
		ll now = query_range_size(i , n , 1 , 0);
		if(now == -1) continue;
		ll h = query_range(now , now , 1);
		ll cal = h / c;
		if(h % c) cal ++;
		int cnt = upper_bound(dist + 1 , dist + n + 2 , a[now].x + d * 2) - dist;
		update_range(now , cnt - 1 , -(cal * c) , 1);
		ans += cal;
	}
	cout << ans << '\n';
	return 0;
}

C

题目大意

给定一个含有 \(n\) 个整数的数组a,将其所有点对\((i,j),i≠j\),两两相乘 \(a[i]×a[j]\) 得到 \(N(N−1)/2\) 个数,问将其升序排序后第 \(k\) 个数是多少?

解题思路

分类讨论 \(+\) 尺取 \(+\) 二分

很显然可以将两两相乘的结果分为三类

  1. 正数 × 负数
  2. 零 × 负数,零 × 正数
  3. 负数 × 负数,正数 × 正数

记负数的个数为 \(f\) , 零的个数为 \(z\),正数的个数为 \(c\)

那么负数的下标区间为 \([1,f]\) , 零的下标区间为 \([f + 1 , f + c]\) , 正数的下标区间为 \([f + c + 1 , n]\)

那么第一类的总数为 \(f×c\),第二类的总数为 \(z×f + z×c\),第三类的总数为 \(f×(f-1)/2 + c×(c-1)/2\)

然后我们先对所有数从小到大排个序 , 再根据 K 的大小进行分类讨论:

第一类

很显然是从区间 \([-INF,-1]\) 二分答案 \(mid\)
但是如何 \(check\) 呢?
不难想到 \(mid\) 一定是由一个正数 × 一个负数构成
于是可以采用尺取法统计小于等于 \(mid\) 的数的个数

int pos = n - c + 1 , sum = 0;
rep(i , 1 , f)
{
	while(pos <= n && a[i] * a[pos] > mid) pos ++;
	if(pos <= n && a[i] * a[pos] <= mid) sum += n - pos + 1;
}

当然如果你觉得尺取法不太好写 or 代码量太大,也是可以采用 lower_bound 来计算的

int sum = 0;
rep(i , 1 , f)
{
	int x = mid / a[i];	
 	if(mid % a[i]) x ++; 
	int cnt = lower_bound(a + 1 , a + 2 + n , x) - a;
   if(cnt > n) continue ;
	sum += n - cnt + 1;		
}

我们只要判断 \(k >= sum\) 是否成立即可写出 \(check\)

第二类

直接输出 \(0\) 即可

第三类

同第一类一样从区间 \([1,INF]\) 二分答案 \(mid\)
但构成的方式有两种会相对复杂一些
1.负数 × 负数 2.正数 × 正数
不过大体思路和第一类差不多,就不细讲了

AC_Code

#include<bits/stdc++.h>
#define int long long 
#define rep(i , a , b) for(int i = a ; i <= b ; i ++)
#define per(i , b , a) for(int i = b ; i >= a ; i --)
using namespace std;
const int INF (0x3f3f3f3f3f3f3f3fll);
const int N = 2e5 + 10;
int a[N] , b[N] , d[N];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	int  n , k;
	int z = 0 , f = 0 , c = 0 , ans;
	cin >> n >> k;
	rep(i , 1 , n)
	{
		cin >> a[i];
		if(!a[i]) z ++ ;
		else if(a[i] > 0) c ++ ;
		else f ++ ; 
	}
	sort(a + 1 , a + 1 + n);
	a[n + 1] = INF;
	if(k <= f * c)
	{ 
		int l = -INF , r = -1 , mid;
		while(l <= r)
		{
			mid = l + r >> 1LL;
			int sum = 0;
			rep(i , 1 , f)
			{
				int x = mid / a[i];
				if(mid % a[i]) x ++; 
				int cnt = lower_bound(a + 1 , a + 2 + n , x) - a;
				if(cnt > n) continue ;
				sum += n - cnt + 1;
			}
			if(sum >= k) r = mid - 1 , ans = mid;
			else l = mid + 1;  
		}
		cout << ans << '\n';
	}
	else if(k > z * (z - 1) / 2 + f * c + z * f + z * c)
	{
		k -= z * (z - 1) / 2 + f * c + z * f + z * c;
		int l = 1 , r = INF , mid , cnt = 0;
		rep(i , 1 , f)
		b[i] = -a[f - i + 1];
		rep(i , f + z + 1 , n)
		d[++ cnt] = a[i];
		while(l <= r)
		{
			mid = l + r >> 1;
			int sum = 0 , pos = f;
			rep(i , 1 , f)
			{
				if(b[i] * b[i] <= mid) sum -- ;
				while(b[i] * b[pos] > mid && pos) pos -- ;
				if(pos && b[i] * b[pos] <= mid) sum += pos;
			}
			pos = c;
			rep(i , 1 , c)
			{
				if(d[i] * d[i] <= mid) sum -- ;
				while(d[i] * d[pos] > mid && pos) pos -- ;
				if(pos && d[i] * d[pos] <= mid) sum += pos;
			}
			if(sum / 2 >= k) r = mid - 1 , ans = mid ;
			else l = mid + 1;
		}
		cout << ans << '\n';
	}
	else  cout << 0 << '\n';
	return 0;
}

D

题目大意

给定一个具有 \(N\) 个顶点的凸多边形,将顶点从 \(1\)\(N\) 标号,每个顶点的权值都是一个正整数。
将这个凸多边形划分成 \(N-2\) 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积
问所有三角形的顶点权值乘积之和最少为多少。

解题思路

区间 DP
将每个顶点进行编号(从 \(1 - n\)),第 \(i\) 个顶点的权值为 \(ai\)
定义 \(dp_{i,j}\) :将顶点 \(i-j\) 所构成的多边形划分成不相交三角形所得到的最小权值乘积
那么不难得到状态转移方程: \(dp_{i,j} = dp_{i,k} + dp_{k,j} + a_i×a_j×a_k\) (见下图)

AC_Code

#include<bits/stdc++.h>
#define int long long 
#define rep(i , a , b) for(int i = a ; i <= b ; i ++)
using namespace std;
const int INF = 0x3f3f3f3f33fll;
const int N = 1e2 + 10;
int n , a[N] , dp[N][N];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	cin >> n;
	rep(i , 1 , n) cin >> a[i];
	for(int len = 2 ; len <= n ; len ++)
	{
		for(int l = 1 ; l + len <= n ; l ++)
		{
			int r = l + len;
			dp[l][r] = INF;
			rep(k , l , r - 1) dp[l][r] = min(dp[l][r] , dp[l][k] + dp[k][r] + a[l] * a[k] * a[r]);
		}
	}
	cout << dp[1][n] << '\n';
	return 0;
}

E

点我跳转

F

题目大意

给定 \(N\) 个灯,每个灯都有两种状态(开/关)
\(i\) 个灯的编号为 \(i\),起初所有灯都是亮的
现在有 \(k\) 次操作,每次操作给定一个数 \(x\) 要求把编号为 \(x\) 的倍数的灯的状态改变 ( 开 → 关,关 → 开)
问整个过程中灯最多关闭了几盏

解题思路

作为签到题,它的考点为 调和级数的复杂度

for(int i = 1 ; i <= n ; i ++)
   for(int j = 1 ; j <= n ; j += i) 
for(int i = 1 ; i <= n ; i ++)
   for(int j = 1 ; i * j <= n ; j ++)

在知道以上复杂度为 \(nlogn\) 后 ,再按题意模拟一遍即可

AC_Code

#include<bits/stdc++.h>
#define rep(i , a , b) for(int i = a ; i <= b ; i ++)
using namespace std;
const int N = 1e6 + 10;
int a[N]; 
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	int n , sum = 0 , ans = 0; 
	cin >> n;
	rep(i , 1 , n) a[i] = 1;
	int k; 
	cin >> k;
	while(k --)
	{
		int x;
		cin >> x;
		for(int i = x ; i <= n ; i += x)
		{
			if(a[i]) sum ++ ;
			else sum --;
			a[i] ^= 1;
		}
		ans = max(ans , sum);
	}
	cout << ans << '\n';
	return 0;
}

G

题目大意

给定一个包含 \(N\) 个数的集合,求这个集合的 \(MEX\)

$ MEX $ 的定义:集合中未出现的最小自然数

解题思路

\(N\) 的范围貌似没给 ?但 \(N\) 给不给其实也无所谓

题目给定的集合内元素的范围是 \(-10^{100}\)~\(10^{100}\)

显然这个范围的元素用整型变量是无法直接储存的,于是读入要用字符串来操作

而一共就 \(N\) 个元素,那么 \(MEX\) 必然出现在 \(0\) ~ \(N\) 之中

所以当读入的字符串的长度是否大于 \(logN\) 时直接跳过对这个数的操作

当读入的字符串长度小于等于 \(logN\) 时,将其转换为数字并存入该数字对应的桶中(桶 = 数组)

最后从 \(0\) ~ \(N\) 遍历所有桶,找到第一个空桶输出其编号即可

AC_Code

#include<bits/stdc++.h>
#define rep(i , a , b) for(int i = a ; i <= b ; i ++)
using namespace std;
const int N = 2e6 + 10;
char s[N];
int n , cnt[N];
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0) , cout.tie(0);
	cin >> n;
	rep(i , 1 , n)
	{
		cin >> s;
		if(s[0] == '-') continue ;
		int m = strlen(s);
		if(m > 6) continue ;
		int x = atoi(s);
		cnt[x] ++ ;
	}
	rep(i , 0 , n) if(!cnt[i]) return cout << i << '\n' , 0;
	return 0;
}
posted @ 2020-09-25 04:35  GsjzTle  阅读(440)  评论(0编辑  收藏  举报