雅禮學習10.1
上午考試解題報告
各題狀況
每道題的難度大概都在noip提高組Day2T2T3難度
T1
只會模擬
模擬寫完之後,小小加了一個優化
期望得分20+20
T2
光是理解題意就花了好長時間。。。
然後勉勉強強算是會了暴力的寫法(複雜度\(O(N!)\))
不會判重,但是lyq說按照他的想法這題根本不需要判重???
T3
我。。。這題徹底理解錯誤了
四聯通是指對於每個格子向外擴展的方向
而實際主角可以每次對一個聯通塊進行操作使其變色
還以爲每次只能處理特定形狀的部分。。。。
GG
各題代碼
T1
/*
* 考慮根據題意模擬
* 對每次詢問都進行[l,r]內的mod操作,取mod後最大值
* 大概有20分
*/
#include <cstdio>
#include <algorithm>
inline int read()
{
int n=0,w=1;register char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')n=n*10+c-'0',c=getchar();
return n*w;
}
inline int max(int x,int y)
{return x>y?x:y;}
const int N=1e5+1;
int n,m,a[N];//,ans[N];
/*struct Query{
int l,r,k,id;
bool operator<(const Query &x)const
{return k<x.k;}
}que[N];*/
int main()
{
freopen("flower.in","r",stdin);
freopen("flower.out","w",stdout);
n=read(),m=read();
for(int i=1;i<=n;++i)
a[i]=read();
for(int ans,l,r,k,i=1;i<=m;++i)
{
ans=0;
l=read(),r=read(),k=read();
for(;l<=r;++l)
ans=max(ans,(a[l]>=k?a[l]%k:a[l]));
printf("%d\n",ans);
}
fclose(stdin);fclose(stdout);
return 0;
}
T2
/*
* 嘗試理解題意
* 。。。
* 理解無能。。。
*
* 等會好像是dfs的方式
* 判重不會搞。
* 那就不搞了.
*/
#include <stack>
#include <cstdio>
inline int read()
{
int n=0,w=1;register char c=getchar();
while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();}
while(c>='0'&&c<='9')n=n*10+c-'0',c=getchar();
return n*w;
}
const int N=6001,mod=1e9+7;
int n,ans,x[N],y[N],lx[N],topx,ly[N],topy;
void dfs()
{
for(int i=1;i<=n;++i)
if(y[i]<ly[topy] && (x[i]<lx[topx] && x[i]>lx[topx-1])
|| (x[i]>lx[topx] && x[i]<lx[topx-1]))
{
ly[++topy]=y[i];
lx[++topx]=x[i];
dfs();
--topx,--topy;
}
++ans;
if(ans==mod)ans=0;
}
void dfsbegin()
{
for(int i=1;i<=n;++i)
if(y[i]<ly[topy])
{
ly[++topy]=y[i];
lx[++topx]=x[i];
dfs();
--topx,--topy;
}
++ans;
}
int main()
{
freopen("refract.in","r",stdin);
freopen("refract.out","w",stdout);
n=read();
for(int i=1;i<=n;++i)
x[i]=read(),y[i]=read();
for(int i=1;i<=n;++i)
{
ly[++topy]=y[i];
lx[++topx]=x[i];
dfsbegin();
--topx,--topy;
}
printf("%d",ans);
fclose(stdin);fclose(stdout);
return 0;
}
T3
/*
* 誰能告訴我樣例是怎麼出來的。。。
*/
#include <cstdio>
#include <cstring>
const int N=51,dx[]={0,1,0,-1},dy[]={1,0,-1,0};
int r,c,ans,sum,color[N][N];
bool map[N][N];
void dfs(int x,int y)
{
color[x][y]=sum;
for(int xx,yy,i=0;i<4;++i)
{
xx=x+dx[i],yy=y+dy[i];
if(xx<1 || xx>r || yy<1 || yy<c)continue;
dfs(xx,yy);
}
}
void work()
{
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j)
if(true);
}
int main()
{
freopen("paint.in","r",stdin);
freopen("paint.out","w",stdout);
scanf("%d%d",&r,&c);
int tot=0;
for(int x,i=1;i<=r;++i)
for(int j=1;j<=c;++j)
{
scanf("%d",x);
map[i][j]=x;
if(!x)++tot;
}
/*if(r<=15 && c<=15)
{
for(int i=1;i<=r;++i)
for(int j=1;j<=c;++j)
if(!color[i][j])
{
++sum;
dfs(i,j);
}
}
else*/
if(r==1)
{
for(int i=1;i<=c;++i)
if(map[1][i]!=map[1][i-1] && map[1][i]!=map[1][i+1])
map[1][i]=map[1][i-1],++ans;
for(int i=1;i<=c;++i)
if(map[1][i])
++++i,++ans;
printf("%d",ans);
}
else printf("%d",tot>4?tot>>1:tot);
fclose(stdin);fclose(stdout);
return 0;
}
正解及代碼
T1
\(20\)分\(n^2\)做法
\(40\)分塊
另外\(20\)分的做法求前綴和,查看某個數字是否存在
當\(k\)比較大的時候就主席樹,當\(k\)比較小的時候因爲主席樹複雜度會爆,用上面另\(20\)分做法
真·正解:
考慮當\(k\)確定的時候如何求解,顯然對於所有形如\([a_k,(a+1)k)\)的值域,最大值一定是最優的
進一步觀察發現,這樣的區間總數只有\(k\times \ln k\)個,考慮分塊,那麼我們可以在\(O(n+k\ln k)\)的時間複雜度內處理出一個塊對於任意\(k\)的答案,詢問的時候複雜度是\(O(mS),(S\text{是塊的大小})\)的,取\(S=\sqrt{k\ln k}\)可以達到最優複雜度\(O(n\sqrt{k\ln k})\)
#include <bits/stdc++.h>
using std::pair;
using std::vector;
using std::string;
typedef long long ll;
typedef pair<int, int> pii;
#define fst first
#define snd second
#define pb(a) push_back(a)
#define mp(a, b) std::make_pair(a, b)
#define debug(...) fprintf(stderr, __VA_ARGS__)
template <typename T> bool chkmax(T& a, T b)
{ return a < b ? a = b, 1 : 0; }
template <typename T> bool chkmin(T& a, T b)
{ return a > b ? a = b, 1 : 0; }
const int oo = 0x3f3f3f3f;
string procStatus() {
std::ifstream t("/proc/self/status");
return string(std::istreambuf_iterator<char>(t),std::istreambuf_iterator<char>());
}
template <typename T> T read(T& x) {
int f = 1; x = 0;
char ch = getchar();
for(;!isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
return x *= f;
}
const int B = 1000;
const int N = 100000;
int n, q;
int a[N + 5];
int lst[N + 5];
int ans[105][N + 5];
int main() {
//freopen("flower.in", "r", stdin);
//freopen("flower.out", "w", stdout);
read(n); read(q);
for(int i = 0; i < n; ++i) read(a[i]);
int blks = (n-1) / B + 1;
for(int i = 0; i < blks; ++i) {
memset(lst, 0, sizeof lst);
for(int j = i * B; j < (i+1) * B && j < n; ++j)
lst[a[j]] = a[j];
for(int j = 1; j <= N; ++j)
chkmax(lst[j], lst[j-1]);
for(int j = 1; j <= N; ++j)
for(int k = 0; k <= N; k += j)
chkmax(ans[i][j], lst[std::min(k + j - 1, N)] - k);
}
while(q--) {
static int l, r, k;
read(l), read(r), read(k);
-- l, -- r;
int x = (l / B) + 1, y = (r / B), res = 0;
if(x == y + 1) {
for(int i = l; i <= r; ++i) chkmax(res, a[i] % k);
} else {
for(int i = x; i < y; ++i) chkmax(res, ans[i][k]);
for(int i = l; i < x*B; ++i) chkmax(res, a[i] % k);
for(int i = r; i >=y*B; --i) chkmax(res, a[i] % k);
}
printf("%d\n", res);
}
return 0;
}
T2
10分和20分其實都是\(n^3\)做法,區別僅在於常數問題
50分是\(n^2\)的\(dp\),但是因爲空間開不下就變成了\(50\)分
100分:上面50分的dp把空間卡成\(\frac{n^2}{2}\)就是100分了
\(dp[i][j]\)表示上一條折線是\(i\rightarrow j\)的
具體地講:
若將所有點按照\(y_i\)的順序進行轉移,有上界和下界兩個限制,優化比較難
那麼考慮按照\(x_i\)排序進行轉移,並且記錄\(f_{i,0/1}\)表示以第\(i\)個點爲頂端接下來向左或者向右的折線方案數,從左到右加點
考慮前\(i\)個點構成的包含\(i\)點的折線,由於新加入的點橫座標是當前考慮的點鐘橫座標最大的,所以只可能是折線的起始點或者第二個點
如圖,假設新加入點\(i\)是折線的起始點,\(k\)是一個符合轉移條件的點(也就是在\(i\)點下方),顯然可行
如圖,\(i\)是當前點,\(j\)是上一個點,\(k\)是一個符合條件的點(在\(i\)點左下方,同時在\(j\)點右下方)
那麼有
- \(\forall y_j\lt y_i,f_{i,0}\leftarrow f_{j,1}\)
- \(\forall y_j\gt,f_{j,1}\leftarrow f_{k,0} | x_k\gt x_j\; and\; y_k\lt y_i\)
第二種情況可以進行前綴和優化,複雜度\(O(n^2)\)
#include <bits/stdc++.h>
using std::pair;
using std::vector;
using std::string;
typedef long long ll;
typedef pair<int, int> pii;
#define fst first
#define snd second
#define pb(a) push_back(a)
#define mp(a, b) std::make_pair(a, b)
#define debug(...) fprintf(stderr, __VA_ARGS__)
template <typename T> bool chkmax(T& a, T b)
{ return a < b ? a = b, 1 : 0; }
template <typename T> bool chkmin(T& a, T b)
{ return a > b ? a = b, 1 : 0; }
const int oo = 0x3f3f3f3f;
string procStatus() {
std::ifstream t("/proc/self/status");
return string(std::istreambuf_iterator<char>(t),std::istreambuf_iterator<char>());
}
template <typename T> T read(T& x) {
int f = 1; x = 0;
char ch = getchar();
for(;!isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
return x *= f;
}
const int N = 6000;
const int mo = 1e9 + 7;
pii p[N + 5];
int dp[N + 5][2], n;
int main() {
//freopen("refract.in", "r", stdin);
//freopen("refract.out", "w", stdout);
read(n);
for(int i = 1; i <= n; i++) {
read(p[i].fst), read(p[i].snd);
}
sort(p + 1, p + n + 1);
for(int i = 1; i <= n; i++) {
dp[i][0] = dp[i][1] = 1;
for(int j = i-1; j >= 1; j--) {
if(p[j].snd > p[i].snd) {
(dp[j][1] += dp[i][0]) %= mo;
}else {
(dp[i][0] += dp[j][1]) %= mo;
}
}
}
int ans = mo - n;
for(int i = 1; i <= n; i++)
ans = ((ans + dp[i][0]) % mo + dp[i][1]) % mo;
printf("%d\n", ans);
return 0;
}
T3
7分輸出黑色聯通塊個數
26分狀壓DP
民間100分做法:每個聯通塊和它相鄰的聯通塊連邊,最後發現是一個圖,如果每個聯通塊只能在圖中出現一次,就會變成一棵樹
然後找最長鏈
正解做法:
可以發現一個“比較顯然”的結論:存在一種最優方案使得每次操作的區域是上一次的子集且顏色和上一次相反
考慮歸納法證明:
- \(T\)與\(S\)有交的情況一定可以轉化成\(T\)被\(S\)包含的情況
- \(T\)與\(S\)交集爲空的時候,可以找一個連接\(S\)和\(T\)的集合\(M\)並操作\(S\cup T\cup M\),並將之前的所有操作連接到更外的層以及外層的連接部分同時操作,特殊處理最外層和第二層的情況
- \(T\)被\(S\)包含的時候,\(T\)落在某個完整區域內時等價於情況\(2\),否則一定連接若干個同色塊,這些塊可以同時處理並且顯然答案不會更劣
那麼我們可以枚舉最後被修改的區域,這時答案就是將同色邊邊權當成\(0\),異色邊邊權爲\(1\),而後舉例這個點最遠的黑色點的距離,對所有點取最小值即可
#include <bits/stdc++.h>
using std::pair;
using std::vector;
using std::string;
typedef long long ll;
typedef pair<int, int> pii;
#define fst first
#define snd second
#define pb(a) push_back(a)
#define mp(a, b) std::make_pair(a, b)
#define debug(...) fprintf(stderr, __VA_ARGS__)
template <typename T> bool chkmax(T& a, T b)
{ return a < b ? a = b, 1 : 0; }
template <typename T> bool chkmin(T& a, T b)
{ return a > b ? a = b, 1 : 0; }
const int oo = 0x3f3f3f3f;
string procStatus() {
std::ifstream t("/proc/self/status");
return string(std::istreambuf_iterator<char>(t),std::istreambuf_iterator<char>());
}
template <typename T> T read(T& x) {
int f = 1; x = 0;
char ch = getchar();
for(;!isdigit(ch); ch = getchar()) if(ch == '-') f = -1;
for(; isdigit(ch); ch = getchar()) x = x * 10 + ch - 48;
return x *= f;
}
const int N = 50;
int n, m;
char g[N + 5][N + 5];
int dis[N + 5][N + 5];
const int dx[] = { 1, 0, -1, 0 };
const int dy[] = { 0, 1, 0, -1 };
int bfs(int x, int y) {
std::deque<pii> q;
memset(dis, -1, sizeof dis);
dis[x][y] = 0;
q.push_back(mp(x, y));
int res = 0;
while(!q.empty()) {
int cx = q.front().fst, cy = q.front().snd;
if(g[cx][cy] == '1')
chkmax(res, dis[cx][cy]);
q.pop_front();
for(int i = 0; i < 4; ++i) {
int nx = cx + dx[i], ny = cy + dy[i];
if(nx >= 0 && nx < n && ny >= 0 && ny < m && dis[nx][ny] == -1) {
if(g[nx][ny] == g[cx][cy]) {
dis[nx][ny] = dis[cx][cy];
q.push_front(mp(nx, ny));
} else {
dis[nx][ny] = dis[cx][cy] + 1;
q.push_back(mp(nx, ny));
}
}
}
}
return res;
}
int main() {
//freopen("paint.in", "r", stdin);
//freopen("paint.out", "w", stdout);
read(n), read(m);
for(int i = 0; i < n; ++i) {
scanf("%s", g[i]);
}
int ans = oo;
for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
chkmin(ans, bfs(i, j));
}
}
printf("%d\n", ans + 1);
return 0;
}
講課(雜題選講)
Rest In The Shades
平面上有一個點光源,以每秒一個單位的速度從\((x_0,y)\)沿直線走到\((x_1,y),y\lt 0\),\(x\)軸上面有\(n\)條線段會遮擋光線,\(x\)軸上方有\(q\)個點。問每個點能夠被光線直射的總時間
\(n,q\le 10^5\)
解:
反過來理解,把\(x\)軸上方的\(q\)個點看做點光源,下面移動的是接收器
那麼要求的就是對於每個點光源,其能照射到接收器的總時間
那麼每個點光源向周圍發射的光經過\(x\)軸上面的線段遮擋之後到下面就形成了兩個三角形(點光源和光在\(x\)軸上照亮的區間、點光源和光在移動路徑上照亮的區間)
因爲移動路徑和\(x\)軸是平行的
考慮根據相似,先求出每個點在\(x\)軸上照亮的區間,再根據比值就求出了在移動路徑上的長度
另外由於可能會出現:接收器並沒有完全經過照亮的區間就停止移動了或者接收器開始移動的位置之前的部分也能夠被照亮
所以對這兩個區間(包含起點和終點的)進行二分,找到不合法(被照亮但是接收器沒有經過這裏)的區間,減掉這部分
用二分的原因:精度差小
Nastya and King-Shamans
給出一個長度爲\(n\)的序列\(\{a_i\}\),現在進行\(m\)次操作,每次操作會修改某些\(a_i\)的值,在每次操作完成之後需要判斷是否存在一個位置\(i\)滿足\(a_i=\sum_{j=1}^{i-1}a_j\)並求出任意一個\(i\)
\(n,m\le 10^5,0\le a_i\le 10^9\)
解:
如果是單點修改的話,考慮維護式子\(a_i=\sum_{j=1}^{i-1}a_j\),變成\(a_i-\sum_{j=1}^{i-1}a_j=0\),顯然對於單點修改的情況下這個非常好維護,直接查詢是不是\(0\)就行了
區間修改的話,首先考慮一個暴力,每次從\(i=1\)開始統計\(\sum_i a_i\)然後判斷是否合法,顯然\(sum_i\)是單調不降的,符合條件的\(a_i\)一定\(\ge sum_i\)每次用線段樹找到大於等於當前前綴和的最左邊的\(a_i\)並且判斷是否合法
因爲“如果一個新的\(a_i\)不合法,那麼當前前綴和至少會達到上一次的兩倍”,那麼這樣查找的複雜度就是\(O(\log\omega)\),\((\omega\)是前綴和的最大值\()\),那麼總複雜度就是\(O(m\log n\log\omega)\)
Raining Season
有一棵\(n\)個點的樹,每條邊有兩個參數\(a_i,b_i\),在某一時刻\(t\),一條邊的長度爲\(a_i\times t+b_i\)
問在不同時刻\(t=0,1,2\cdots,m-1\)這些時刻樹的直徑
\(n,m,a_i\le 10^5,b_i\le 10^9\)
解:
考慮已經得到若干條路徑後如何計算答案, 將每條路徑表示成數對\((a_i,b_i)\), 從小到大枚舉 \(t\) , 直徑的 \(a_i\) 一定單調不降, 並且變化過程可以用 斜率優化維護.
\(a_i\times t+b_i\ge a_j\times t+b_j\rightarrow (a_i-a_j)\times t\ge b_j-b_i\)
\(-t\le \frac{b_i-b_j}{a_i-a_j}\)
發現是一個類似上凸殼的轉移
但是這樣的複雜度是\(O(n^2)\),複雜度依然是錯的
顯然我們需要的複雜度是\(O(n\sqrt n)\)或者\(O(n\log n)\)
問題變成如何減小路徑數量, 比較容易想到的方法是樹分治
由於跨越中心的兩部分需要合併, 我們採用邊分治
跨越中心的部分就是兩個部分的點的閔科夫斯基和.
$Minkowski Sum $
定義: 給出兩個點集\(A,B\), 求點集$ C ={a+b | a\in A,b\in B}$的凸包.
解法: \(C\)中凸包上的點由\(A,B\)凸包上的點相加得到, 先求出\(A,B\)的凸包. 找到\(A,B\)一組相加後在凸包上的點, 向後按照相鄰向量的極角順序合併.
Make Symmetrical
給一個初始爲空的整點集, 現在有\(n\)次操作:
- 1 x y向點集中插入\((x,y)\)
- 2 x y從點集中刪除\((x,y)\)
- 3 x y詢問至少需要添加多少個點滿足這個點集形成的圖像關於\((0,0),(x,y)\)的連線對稱
\(n\le 2\times 10^5,0\le x,y\le 10^5\)
解:
注意到互相對稱的兩點到達原點的距離相同,即互相對稱的兩點在同一個圓的圓周上
因爲這些點對都是整數,而圓上整數點的規模可以看做\(\sqrt n\)(實際遠小於\(\sqrt n\),詳細可見《隱藏在素數規律中的\(\pi\)》)
對於每個圓,維護其邊界上點的集合,每加入或者刪除一個點就計算 這個點和其它點的對稱中心並統計答案
複雜度可以認爲是\(O(n\sqrt n\log n)\)但是難以達到理論上界
Number Clicker
給出三個數\(u,v,p\),可以對\(u\)進行如下操作
- \(u\leftarrow u-1\mod p\)
- \(u\leftarrow u+1\mod p\)
- \(u\leftarrow u^{p-2}\mod p\)
求一種不超過\(200\)步的方案使得\(u\)最終變成\(v\)
\(0\le u,v\lt p\le 10^9\),\(p\)是質數
解:
操作\(3\)也就是費馬小定理求逆元
因爲操作\(3\)的存在, 我們可以近似地認爲這張圖是隨機的, 但是直接搜的期望複雜度還是很大
考慮雙向\(BFS\),先從\(u\)出發進行\(BFS\)並取出前\(\sqrt p\)個狀態的值,顯然這部分路徑的長度不會超過\(100\)
接下來從\(v\)出發進行\(BFS\)並且在之前的答案中查詢,根據生日悖論,\(\sqrt p\)次找不到解的概率接近\(0\)
生日悖論
一個班級,\(30\)個人中,有兩人生日相同的概率接近\(100%\)
證明:
考慮所有人中沒有人的生日相同的概率
第一個人與第二個人生日不同的概率爲\(\frac{364}{365}\),第三個人不與前兩人生日相同的概率爲\(\frac{363}{365}\),第四人\(\cdots\),最後當人數爲\(30\)的時候,可得三十個人生日兩兩不同的概率=\(1\times \frac{364}{365}\times \frac{363}{365}\times \cdots\frac{335}{365}\)
這個值經過收斂,非常接近\(0\)
因爲"所有人中沒有人生日相同"是"有至少兩人生日相同"的對立事件
所以“至少兩人生日相同“這一事件的概率是接近\(100%\)的
Distinctification
給出一個長度爲\(n\)的序列\(x\),每個位置有\(a_i,b_i\)兩個參數,\(b_i\)互不相同,可以進行任意次如下操作:
- 若存在\(j\ne i\)滿足\(a_j=a_i\),則可以花費\(b_i\)的代價讓\(a_i+1\)
- 若存在\(j\)滿足\(a_j+1=a_i\),則可以花費\(-b_i\)的代價讓\(a_i-1\)
定義一個序列的權值爲將序列中所有\(a_i\)變得互不相同所需的最小代價。
問給定序列的每一個前綴的權值
\(n,a_i\le 2\times 10^5,1\le b_i\le n\)
解:
先考慮所有\(a_i\)互不相同的時候怎麼做,若存在\(a_i+1=a_j\),則可以花費\(b_i-b_j\)的代價交換兩個\(a_i\),顯然最優方案會將序列中所有\(a_i\)連續的自斷操作成按\(b_i\)降序的
如果有\(a_i\)相同,則可以先將所有\(a_i\)編程互不相同的再進行排序,但是這時候可能會擴大值域使得原本不連續的兩個區間合併到一起,於是我們需要維護一個支持合併的數據結構
嘗試用並查集維護每個值域連續的塊,並且在每個並查集的根上維護一個以\(b\)爲關鍵字的值域線段樹,每次合併兩個聯通塊的時候,合併他們對應的線段樹即可維護答案
Scissoros
有兩個串\(S,T\)和一個數字\(k\),可以將\(S\)中任意兩個長度爲\(k\)的不相交子串取出,並且按照順序拼接到一起,求一種取串的方案使得\(T\)是這個新串的子串
\(|T|\le 2k\le |S|\le 5\times 10^5\)
解:
\(T\)一定存在某個分段點使得這個點的左邊部分和右邊部分分別是兩個不相交子串的一個後綴和前綴,考慮枚舉這個分段點,那麼顯然只需要滿足左邊部分第一次出現的標號小魚右邊部分最後一次出現的標號即可
進一步觀察發現,對應\(T\)的所有前綴,第一次出現的位置是單調的,我們只需要做一次\(KMP\)即可得到美國前綴出現的最早的位置