1. 程式人生 > >機房模擬的日常20180922

機房模擬的日常20180922

日常機房模擬 今天不是停課…所以所有人都來了 有一道T1,本來想到了正解,結果忘了可以啟發式合併,覺得時間複雜度過不去就否定掉了 T2想了2個半小時,最後交了一個自己認為錯的程式結果A了(下午發現是正解2333333) T3沒時間了,辣雞騙分苟過了10pts233333 T1

在這裡插入圖片描述 題意簡述: 維護一個數據結構,支援兩種操作 1、將一張圖中任意兩點之間連一條邊 2、查詢某一次操作後某一個點所在的聯通塊的大小 我們可以很自然的想到可持久化並查集 雖然正解可以不用可持久化並查集,但是可持久化並查集肯定是能A的(把修改siz和fat的部分寫兩個函式,用主席樹搞一搞應該能出來) 然而 開考的時候半個小時教練都沒有斷網,然後我們班一個妹子在這個時間裡打開了一篇部落格把可持久化並查集學習了,隨後A掉了這道題 正解是並查集加二分…(思想有點類似於時間分治) 由於我們每一次的查詢都僅僅針對任何一個點 那麼我們考慮將每一個點被修改時候的情況儲存起來。 我們發現每一個點的查詢只跟它最後一次被修改的時間有關係,那麼我們對於任意一個點,在其被修改以後記錄下這次被修改後的並查集的根節點。 然後我們再對每一個節點開一個vector,vector裡儲存二元組,分別表示該節點是哪次被修改,以及那一次被修改過後的siz。 稍微想一想就可以發現單次合併的時候並不需要把兩個並查集裡面的所有點的資訊都加入vector(考試的時候就是栽在這裡,認為需要全部push進去導致否認了這個本來正確的演算法),我們可以只將根節點的資訊push進去,然後合併並查集的時候不壓縮路徑

這個是重中之重,因為我們可以利用啟發式合併的性質來保證複雜度。 我們可以發現,如果不壓縮路徑的話,我們就可以完成以下的操作: 當要查詢一個節點的時候,一直跳該節點的父親節點,而我們已經事先記錄下來該節點最後一次修改實在什麼時候,那麼我們假設跳到某一個節點u的時候,該節點的修改時間已經比我們要查詢的時間更在後面了的話,那麼就說明我們需要查詢的資訊就在這個根節點上面。 而我們每一次合併的時候都會把資訊儲存在根節點裡面,所以我們跳到的點上面肯定會擁有那一次修改之後的資訊。 所以對於每一個並查集都要多記錄一個並查集的深度,每次將深度淺的合併到深的那裡,否則的話是無法保證複雜度的,這叫做啟發式合併(這樣的操作比起路徑壓縮來說其實是非常慢的,雖然可以證明均攤後的複雜度是log,但是如果說出題人真的很想卡你,它可以把這個“均攤”變成“一次"23333) 其實程式碼比本蒟蒻講解的更好… 所以獻上醜陋無比的程式碼

#include<cstdio>
#include<vector>
#include<iostream>
#include<algorithm>
#define OP "build"
const int MAXN=1e5+5;
int read()
{
    int X=0,w=0;
	char ch=0;
    while(!isdigit(ch)) {w|=ch=='-';ch=getchar();}
    while(isdigit(ch)) X=(X<<3)+(X<<1)+(ch^48),ch=getchar()
; return w?-X:X; } class Node { public: int tim; int siz; bool operator<(const Node &z)const { return tim<z.tim; } }; int lastans; int siz[MAXN]; int f[MAXN]; int dep[MAXN]; int t[MAXN]; std::vector<Node>V[MAXN]; int find(int x) { return x==f[x]?x:find(f[x]); } void unionn(int u,int v,int i) { if(find(u)==find(v)) { return ; } int fu=find(u); int fv=find(v); if(dep[fu]<dep[fv]) { std::swap(fu,fv); } f[fv]=fu; siz[fu]+=siz[fv]; if(dep[fu]==dep[fv]) { dep[fu]++; } V[fu].push_back((Node){i,siz[fu]}); t[fv]=i; } int solve(int u,int times) { while(u!=f[u]&&t[u]<=times) { u=f[u]; } std::vector<Node>::iterator it=std::upper_bound(V[u].begin(),V[u].end(),(Node){times,0}); it--; return it->siz; } int main() { std::freopen(OP".in","r",stdin); std::freopen(OP".out","w",stdout); int n,m; n=read(); m=read(); for(int i=1;i<=n;i++) { f[i]=i; dep[i]=siz[i]=1; V[i].push_back((Node){0,1}); } for(int i=1;i<=m;i++) { int opt,x,y; opt=read(); x=read(); y=read(); int u=x+lastans; int v=y+lastans; if(opt==1) { unionn(u,v,i); } else { std::printf("%d\n",lastans=solve(v,u)); } } return 0; } /* 5 5 1 1 2 2 0 1 1 1 2 1 0 4 2 2 1 */

好的A掉了2333333(考場上沒有想到這一點,最後苟了特殊資料50pts) T2 在這裡插入圖片描述 題意簡述: 有一個全部為正整數的序列,你和另外一個人輪流按序列順序取數,當輪到你取的時候,對於任意的一個數,你可以通過把它送給你的對手並且借之以獲得下一次繼續取的權利,或者取走這一個數之後,下一次取數的權利交給對手,每一次只能取走或送掉這一個序列的第一個數。問你雙方都採取最優策略的情況,你最多能夠獲取多少利潤。 很有博弈性質的一道dp的題… 開始的時候以為dirty只會固定選擇pure選了之後的時候的那一個物品,然後好奇這道題為什麼這麼水… 後來發現兩個人都採取了最優策略,所以我們的dp其實不是很好想 這道題擁有一個天坑,那就是如果你把dp方程定義為dp[i]表示兩人互相分配到第i個城市的時候pure能夠獲得的最大收益,你這輩子都推不出來 因為一個問題能夠使用dp來解決的前提條件是它要有無後效性,然而你正著推並沒有(非常顯而易見…) 所以我們應該定義dp[i]表示從第i個開始取取到最後一個的最大收益,那麼狀態的轉移只有兩種情況 我們首先定義一個sum陣列記錄從i到n的字尾和 1、我們要選擇第i個物品,那麼dp[i]=sum[i+1]-dp[i+1]+v[i] 2、不選擇這個物品,那麼dp[i]=dp[i+1] 所以dp[i]=std::max(dp[i+1],sum[i+1]-dp[i+1]+v[i]). 程式碼實在是太SB了…(考場上問題想複雜了,定義了二維)

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define OP "distribute"
#define LL long long
const int MAXN=1e5+5;
int a[MAXN];
LL dp[MAXN][2];
LL sum[MAXN];
int main() 
{
	std::freopen(OP".in","r",stdin);
	std::freopen(OP".out","w",stdout);
	int n;
	std::scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",a+i);
	}
	dp[n][0]=dp[n][1]=sum[n]=a[n];
	for(int i=n-1;i>0;i--) 
	{
		sum[i]=sum[i+1]+a[i];
		dp[i][0]=std::max(dp[i+1][0],sum[i+1]-dp[i+1][1]+a[i]);
		dp[i][1]=std::max(dp[i+1][1],sum[i+1]-dp[i+1][0]+a[i]);
	}
	return std::printf("%lld",dp[1][0]),0;
}

`` 於是T2成為今天唯一一道輕鬆愉快 切掉的題目(其實花了2h,主要是一開始題意理解錯誤,然後還給那個錯誤的程式寫了一份對拍程式碼,實在是很傷) T3

在這裡插入圖片描述 題意簡述: 給你一個無向圖,問你其中是否存在簡單環,若存在則輸出所有簡單環上面的所有邊,否則就輸出0。 本來沒有人真正想到正解, 但是又倆人,最後交卷之前,抱著騙分的心理(寫不來暴力但是又不想T3爆炸),寫了一個割點求邊雙,竟然奇蹟般的過了樣例(實際上是他們倆發現了邊雙可以過樣例才這麼寫…),然後交上去之後 A了… A了… A了… woc…真的是倆小天才(我真是想破腦子也沒有想出來邊雙…) 所以我的考場程式碼是這個樣子的

#include<cstdio>
#define OP "find"
int main()
{
		std::freopen(OP".in","r",stdin);
		std::freopen(OP".out","w",stdout);
		std::puts("0\n");
	return 0;
}

具體的證明過程比較複雜,但是我們的題解上面寫的很簡單…所以我就直接把solution貼上來吧 在這裡插入圖片描述 在這裡插入圖片描述 在做這道題之前我一直沒有意識到自己的圖的連通性學得巨tm差… 然後我終於會求割點和橋了2333333333(去年就講了解法然後我現在才會)

#include<cstdio>
#include<algorithm>
#define OP "find"
const int MAXN=1e5+5;
class Edge
{
	public:
		int nxt;
		int to;
}edge[MAXN<<1];
int head[MAXN];
int num=1;
void add(int from,int to)
{
	edge[++num].nxt=head[from];
	edge[num].to=to;
	head[from]=num;
}
int low[MAXN];
int dfn[MAXN];
bool vis[MAXN<<1];
bool ok[MAXN<<1];
int stk[MAXN<<1];
int indx=0;
int top=0;
int color[MAXN];
int cnt=0;
int vc[MAXN<<1];
void tarjan(int x,int fe)
{
	indx++;
	dfn[x]=low[x]=indx;
	for(int i=head[x];i;i=edge[i].nxt)
	{
		int v=edge[i].to;
		if((i^1)==fe)
		{
			continue;
		}
		if(!vis[i])
		{
			vis[i^1]=vis[i]=1;
			stk[++top]=i;
		}
		if(!dfn[v])
		{
			tarjan(v,i);
			low[x]=std::min(low[x],low[v]);
			if(low[v]>=dfn[x])
			{
				int nump=0;
				int nume=0;
				cnt++;
				while(1)
				{
					int e=stk[top--];
					if(color[edge[e].to]!=cnt)
					{
						color[edge[e].to]=cnt;
						nump++;
					}
					if(color[edge[e^1].to]!=cnt)
					{
						color[edge[e^1].to]=cnt;
						nump++;
					}
					vc[++nume]=e;
					if(e==i)
					{
						break;
					}
				}
				if(nump==nume)
				{  
					for(int i=1;i<=nume;i++)
					{
						ok[vc[i]]=ok[vc[i]^1]=1;
					}
				}
			}
		}
		else 
		if(dfn[v]<low[x]){
			low[x]=std::min(low[x],dfn[v]) ;
		}
	}
}
int main()
{
	std::freopen(OP".in","r",stdin);
	std::freopen(OP".out","w",stdout);
	int n,m;
	std::scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int u,v;
		std::scanf("%d%d",&u,&v);
		add(u,v);
		add(v,u);
	}
	for(int i=1;i<=n;i++)
	{
		if(!dfn[i])
		{
			tarjan(i,0);
		}
	}
	int ans=0;
	for(int e=2;e<=num;e+=2)
	{
		if(ok[e])
		{
			ans++;
		}
	}
	std::printf("%d\n",ans);
	for(int e=2;e<=num;e+=2)
	{
		if(ok[e])
		{
			std::printf("%d ",e/2);
		}
	}
	std::puts("\n");
	return 0;
}

最後得分還被卡掉了20pts,只有140…