2017 NOIp提高组 DAY2 试做

衔接 ( ext{DAY1})->(2017) ( ext{NOIp}) 提高组 ( ext{DAY1}) 试做

D2T1 奶酪

题库原题,很裸的并查集,就是建边相对复杂一些

D2T2 宝藏

题目描述

下方传送门
题目链接
上方传送门

思路分析

  • 刚做的时候太着急了,上去就试着糊了个 ( ext{BFS}),然后 ( ext{get})(40pts)

  • 后来看一眼数据范围,(n) 才这么小,直接暴搜,贪心和剪枝都不需要,直接枚举所有情况(全排列)就行,喜提 (70pts)

  • 那要想 (A) 掉这道题,显然如果还是直接暴搜是过不了 (12) 这个点的(不过好像有大佬暴搜剪枝给过了),但是这样做至少正确性是有保证的,所以我们考虑如何能更快一些

  • 再看一眼数据范围,正如林sir所说:

    这数据范围不状压吗,疯狂暗示

    再如学长说的:

    ——你们不会有人枚举全排列直接暴搜吧?
    ——那用啥?
    ——状压呀。

  • 所以直接用状压再优化一下就好了,小 (DP)

Code

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#define N 20
using namespace std;
inline int read(){
	int x = 0,f = 1;
	char ch = getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int inf = 0x3f3f3f3f;
int n,m,e[N][N],dis[N],dp[1<<N],ans=inf;
bool flag[N][N];
void dfs(int x){
	for(int i = 1;i <= n;i++){ //枚举未访问的点是由哪一个点转移过来的即可
		if(1<<(i-1)&x){
			for(int j = 1;j <= n;j++){
				if(!(1<<(j-1)&x)&&flag[i][j]){
					if(dp[1<<(j-1)|x]>dp[x]+dis[i]*e[i][j]){//
						int pro = dis[j];
						dis[j] = dis[i]+1;
						dp[1<<(j-1)|x]=dp[x]+dis[i]*e[i][j];
						dfs(1<<(j-1)|x);
						dis[j] = pro; //搜完记得回溯
					}
				}
			}
		}
	}
}
int main(){
	memset(e,0x3f,sizeof(e));
	n = read(),m = read();
	for(int i = 1;i <= m;i++){
		int u,v,w;u = read(),v = read(),w = read();
		e[u][v] = e[v][u] = min(e[u][v],w);
		flag[u][v] = flag[v][u] = 1;
	}
	for(int i = 1;i <= 12;i++){
		memset(dis,0x3f,sizeof(dis));
		memset(dp,0x3f,sizeof(dp));
		dis[i] = 1;
		dp[1<<(i-1)] = 0;
		dfs(1<<(i-1));
		ans = min(ans,dp[(1<<n)-1]);
	}
	printf("%d
",ans);
	return 0;
}

D2T3

题目描述

下方传送门
题目链接
上方传送门

思路分析

  • 暴力,30分,然后就不会了
  • 当然还是要有钻研精神的,于是继续边看题解边
  • 对于行来说,每一行都有机会移动,然而对于列来说,移动的只有最后一列
  • 每次移动,其实都相当于是一个删除操作再加上一个插入操作,所以可以用平衡树或者动态开点的线段树来维护(树状数组也彳亍,但我不会)
  • 然后将行和列分开建树,每一行建一个,列只建最后一列就行了,然后考虑各种操作
    • 查询和删除:,这两个是同步进行的,因为每次查询都对应一次删除。用线段树记录相应区间的删除个数,此时区间内的点数就是区间大小减去删除的。然后接下来考虑再删的时候,和查 (K) 大相似如果要删的数小于区间点数,就去左子树删,否则去右子树删
    • 插入:开一个vector将所有插入的数装下就好
    • 查询答案:
      • 移动列,查询这一列线段树的第 (x) 个的位置 (pos),如果 (pos<=n) 说明是原来的移上去的,答案就是 (pos*m)。否则就是后来插进去的,输出 (vector) 里相应的元素即可
      • 移动行,和上面的大同小异,行列颠倒即可,注意维护行的线段树内原有的节点是每一列的前 (m-1) 个,不包含最后一列

Code

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
#define N 300010
#define int long long
#define R register
using namespace std;
inline int read(){
	int x = 0,f = 1;
	char ch = getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
int n,m,q,sum[N*20],lch[N*20],rch[N*20],rt[N],mx,pos,cnt;
vector<int>vc[N];
void modify(int &u,int l,int r){ //更新删除的点
	if(!u)u = ++cnt;
	sum[u]++;
	if(l==r)return;
	int mid = (l+r)>>1;
	if(pos<=mid)modify(lch[u],l,mid);
	else modify(rch[u],mid+1,r);
}
int query(int u,int l,int r,int w){ //查询位置
	if(l==r)return l;
	int mid = (l+r)>>1;
	int tmp = mid-l+1-sum[lch[u]];
	if(w<=tmp)return query(lch[u],l,mid,w);
	else return query(rch[u],mid+1,r,w-tmp);
}
int get_ans(int x,int y){ //移动列
	pos = query(rt[n+1],1,mx,x);
	modify(rt[n+1],1,mx);
	int res = pos<=n ? pos*m : vc[n+1][pos-n-1];
	vc[n+1].push_back(y?y:res);
	return res;
}
int get_ans1(int x,int y){ //移动行
	pos = query(rt[x],1,mx,y);
	modify(rt[x],1,mx);
	int res = pos<m? (x-1)*m+pos : vc[x][pos-m];
	vc[x].push_back(get_ans(x,res)); //再移一下列
	return res;
}
signed main(){
	n = read(),m = read(),q = read();
	mx = max(n,m)+q;
	for(int i =1;i <= q;i++){
		int x,y;x = read(),y = read();
		printf("%lld
",y==m ? get_ans(x,0) : get_ans1(x,y));
	}
	return 0;
}

总结

  • (T1) 并查集还算是比较水的
  • (T2) 暴力分还是很足,当然正解也就是优化一下,并无太大本质区别
  • (T3) 就有点ex了,30分有手就行,正解相当不友好,虽然线段树的代码很短但是不太好想,毕竟压轴题
  • 再结合一下 (D1) 的,不翻车的话 (300) 分以上应该没啥问题, (T1)(A) 掉的话在适当拿一些暴力即可,发挥好可以到 (400) 分左右,不过可能存在误差,毕竟环境不太一样

附洛谷对应题单->2017 NOIp

原文地址:https://www.cnblogs.com/hhhhalo/p/13515483.html