1. 程式人生 > 實用技巧 >2017 NOIp提高組 DAY2 試做

2017 NOIp提高組 DAY2 試做

銜接 \(\text{DAY1}\)->\(2017\) \(\text{NOIp}\) 提高組 \(\text{DAY1}\) 試做

D2T1 乳酪

題庫原題,很裸的並查集,就是建邊相對複雜一些

D2T2 寶藏

題目描述

下方傳送門
題目連結
上方傳送門

思路分析

  • 剛做的時候太著急了,上去就試著糊了個 \(\text{BFS}\),然後 \(\text{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\n",ans);
	return 0;
}

D2T3

題目描述

下方傳送門
題目連結
上方傳送門

思路分析

  • 暴力,30分,然後就不會了
  • 當然還是要有鑽研精神的,於是繼續邊看題解邊
  • 對於行來說,每一行都有機會移動,然而對於列來說,移動的只有最後一列
  • 每次移動,其實都相當於是一個刪除操作再加上一個插入操作,所以可以用平衡樹或者動態開點的線段樹來維護(樹狀陣列也彳亍,但我不會)
  • 然後將行和列分開建樹,每一行建一個,列只建最後一列就行了,然後考慮各種操作
    • 查詢和刪除:,這兩個是同步進行的,因為每次查詢都對應一次刪除。用線段樹記錄相應區間的刪除個數,此時區間內的點數就是區間大小減去刪除的。然後接下來考慮再刪的時候,和查 \(K\) 大相似如果要刪的數小於區間點數,就去左子樹刪,否則去右子樹刪
    • 插入:開一個vector將所有插入的數裝下就好
    • 查詢答案:
      • 移動列,查詢這一列線段樹的第 \(x\) 個的位置 \(pos\),如果 \(pos<=n\) 說明是原來的移上去的,答案就是 \(pos*m\)。否則就是後來插進去的,輸出 \(vector\) 裡相應的元素即可
      • 移動行,和上面的大同小異,行列顛倒即可

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\n",y==m ? get_ans(x,0) : get_ans1(x,y));
	}
	return 0;
}

總結

  • \(T1\) 並查集還算是比較水的
  • \(T2\) 暴力分還是很足,當然正解也就是優化一下,並無太大本質區別
  • \(T3\) 就有點ex了,30分有手就行,正解雖然線段樹的程式碼很短但是不太好想,畢竟壓軸題
  • 再結合一下 \(D1\) 的,不翻車的話 \(300\) 分以上應該沒啥問題, \(T1\)\(A\) 掉的話在適當拿一些暴力即可,發揮好可以到 \(400\) 分左右,不過可能存在誤差,畢竟環境不太一樣

附洛谷對應題單->2017 NOIp