ACM暑期多校聯合賽 題解
6950 Mod, Or and Everything
簽到題
題意:給定一個整數N,求(n mod 1) or (n mod 2) or ... or (n mod (n - 1)) or (n mod n).
思路:
“肯定是先打表找規律啊!”
於是,我們會發現:
當N和答案的關系是,求K = log(n - 1)/log(2) + 1(一個K,使得N 恰好 小於 (1<<K))
答案即為(1ll<<(K - 1) - 1)
注意,我們需要特判一下n = 1的情況(顯然!log(0)是不存在的!)
代碼:
點擊查看代碼
#include
#include
using namespace std;
typedef long long ll;
int main(){
int t;
scanf("%d",&t);
while(t--)
{
ll n;
cin>>n;
if(n == 1)
{
printf("0\n");
continue;
}
n = log(n - 1)/log(2) + 1;
cout<<((1ll<<(n - 1)) - 1)<<endl;
}
return 0;
}
6954 Minimum spanning tree
簽到題
題意:給定一個數N,求2~N中所有點構成一顆最小生成樹,使得其每條邊的權值和最小(廢話),每條邊的權值定義為lcm(a,b)(a,b的最小公倍數),a,b分別為一條邊的兩個節點編號
思路:仔細分析我們可以得到一下結論
如果一個數是質數,那么與它連的邊的權值至少是 這個質數*2
如果一個數不是質數,那么與它連的邊的權值至少是這個數
比如一個數15,我們既然能夠取到15這個數,那么也就必然存在3 5這兩個結點,那么我們只需要將15連上3 or 5就能保證連了15這個節點的邊最小了
蕪湖,這樣我們就貪心得到了一顆最小生成樹啦!
方法:
O(1e7)歐拉篩篩質數
O(1e7)打表存ans數組
O(q)輸出詢問
代碼:
點擊查看代碼
#include <iostream>
#include <cmath>
using namespace std;
typedef long long ll;
const int MAXN = 1e7 + 7;
int prime[MAXN];
int p[MAXN],top = 0;
ll ans[MAXN];
void ini()
{
for(int i = 2;i < MAXN;i++)
{
if(!prime[i]) p[++top] = i;
for(int j = 1;j <= top && p[j]*i < MAXN;j++)
{
prime[p[j]*i] = 1;
if(i%p[j] == 0) break;
}
}
ans[2] = 0;
for(int i=3;i < MAXN;i++){
if(!prime[i]) ans[i] = ans[i - 1] + i*2;
else ans[i] = ans[i - 1] + i;
}
}
int main(){
int t;
scanf("%d",&t);
ini();
while(t--)
{
ll n;
cin>>n;
cout<<ans[n]<<endl;
}
return 0;
}
6955 Xor sum
字典樹
題意:給定一段長為N的序列,求其中一段最小的子序列,使得這段子序列的異或和大於給定的K。輸出子序列的左右端點。
思路:
首先這道題肯定不能O(n^2)
問題轉化:由於xor具有自反性,那么對於一段異或前綴和s[j]和s[i],s[i]^s[j] = a[i +1]^a[i + 2]^... ^a[j];
so我們就能將問題轉化為求一串序列中,尋找間隔最小的兩個數,使得兩個數的異或值大於給定的K。輸出這兩個數的下標。
考慮字典樹,枚舉每一個右端點,這個端點之前的所有數已加入到字典樹中。
我們貪心地去尋找與當前右端點的數A異或最大的那個在它左邊的數,返回下標,如果這個最大值大於K的話更新答案。
這里說下為什么每次貪心找異或最大的那個數(其實只要大於K都行)就能保證是最優的:
似乎是構造這樣一個測試點很困難(我也沒構造...本蒟蒻也不會證QAQ,隊友過了我都是蒙的...)
代碼:
點擊查看代碼
#include <iostream>
#include <cstring>
#include <cmath>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 7;
const int inf = 1e9 + 7;
int t[MAXN*31][3],tot = 0;
int a[MAXN];
void ins(int x,int idx)
{
int p = 0;
for(int i = 30;i >= 0;i--)
{
int u = x >> i & 1;
if(!t[p][u]) t[p][u] = ++tot;
p = t[p][u];
}
t[p][2] = idx;
}
int Go(int x)
{
int p = 0;
for(int i = 30;i >= 0;i--)
{
int u = x >> i & 1;
if(t[p][!u]) p = t[p][!u];
else p = t[p][u];
if(!p)
return -1;//說明不存在
}
return t[p][2];
}
int main(){
int tt;
scanf("%d",&tt);
while(tt--)
{
for(int i = 0;i <= tot;i++)
t[i][0] = t[i][1] = t[i][2] = 0;
tot = 0;
int n,k;
scanf("%d %d",&n,&k);
int ls = 0;
int le,ri;
int ans = inf;
for(int i = 1;i <= n;i++)
{
int x;
scanf("%d",&x);
ls ^= x;
a[i] = ls;
int now = Go(ls);
if((a[now]^ls) >= k)
{
if(i - now < ans)
{
ans = i - now;
le = now + 1;
ri = i;
}
}
ins(ls,i);
}
if(ans == inf)
puts("-1");
else
printf("%d %d\n",le,ri);
}
return 0;
}
這里有個小技巧,就是每次清空字典樹。如果我們直接調用memset對所有的節點清空的話,每次都會花費31*1e5,有些耗時。
其實我們每次只要清空之前使用過的那個節點就行。
6957 Maximal submatrix
單調棧
題意:給定一個矩陣,求一個最大的子矩陣(最大面積),使得子矩陣中每列元素都是遞增的......
思路:
如果我們把輸入的那個矩陣按照 列的遞增個數前綴和 來表示的話,會得到差不多這樣的一個矩陣:
原矩陣:
1 2 4
2 3 3
列遞增前綴和矩陣(我自己取得名字方便描述)
1 1 1
2 2 1
我們會發現對於每一行會記錄下之前行遞增的個數,這有利於我們求面積!
我們考慮一下再這一行的數字的遞增性質,會發現和單調棧的模板題非常像!(其實就是...)
我們直接類比過來即可...
於是問題轉化為枚舉每一行,求這一行能夠構成的最大子矩陣面積,然后再更新最終的答案。
代碼:
點擊查看代碼
#include <iostream>
using namespace std;
const int MAXN = 2e3 + 7;
int n,m;
int a[MAXN][MAXN];
int sum[MAXN][MAXN];
int stk[MAXN],top = 0;
int w[MAXN];
int now[MAXN];
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d %d",&n,&m);
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
scanf("%d",&a[i][j]);
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
{
if(a[i][j] >= a[i - 1][j])
sum[i][j] = sum[i - 1][j] + 1;
else
sum[i][j] = 1;
}
int res = 0;
for(int i = 1;i <= n;i++)
{
top = 0;
for(int j = 1;j <= m;j++)
now[j] = sum[i][j];
int ans = 0;
for(int j = 1;j <= m;j++)
{
int x = now[j];
if(top == 0 || stk[top] <= x)
stk[++top] = x,w[top] = 1;
else
{
int wid = 0;
while(top && stk[top] > x)
{
wid += w[top];
ans = max(ans,wid*stk[top]);
top -= 1;
}
stk[++top] = x;
w[top] = wid + 1;
}
}
int wid = 0;
while(top){
wid += w[top];
ans = max(ans,stk[top]*wid);
top -= 1;
}
res = max(ans,res);
}
printf("%d\n",res);
}
return 0;
}
6958 KD-Graph
貪心+並查集
題意:給定一個圖,求一個長度D,使得這個圖可以分成K份(割出點分成每一份),滿足每一份之中的兩兩之間的距離 <= D,而份之間距離要 > D(當然也可以兩個點不可達)
思路:
由於需要將所有的點進行分份,然后滿足一定條件的邊的權值太會加入到圖中,於是我們可以從大到小枚舉每一條邊,然后每次將當前權值相同的所有邊加入圖中,如果某一次加完當前階段的所有邊(權值相同的邊作為一個合並階段),圖中的點恰好分成了K份,那么這個邊的權值就是答案!
感性證明:
在這個D之前的所有邊都會加入到圖中,使得我們的圖中恰好存在K塊,使得塊與塊之間不可達,而塊內相互可達!這是毫無疑問的。
而后我們加入剩下的所有邊,這些邊不會影響塊與塊之間的可達性(因為大於D,使得我們可以不承認塊連接起來了!)
證畢。
代碼:
點擊查看代碼
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 500005;
struct node
{
int x,y,w;
}a[MAXN];
int n,m,k;
bool cmp(node a,node b)
{return a.w < b.w;}
int fa[MAXN/5];
void ini()
{
int Max = MAXN/5;
for(int i = 0;i < Max;i++)
fa[i] = i;
}
int Find(int x)
{
if(x == fa[x]) return x;
return fa[x] = Find(fa[x]);
}
bool merge(int x,int y)
{
x = Find(x);
y = Find(y);
if(x != y)
{
fa[x] = y;
return true;
}
return false;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
ini();
scanf("%d %d %d",&n,&m,&k);
for(int i = 0;i < m;i++)
scanf("%d %d %d",&a[i].x,&a[i].y,&a[i].w);
if(n == k)
{
puts("0");
continue;
}
sort(a,a + m,cmp);
int now = n;
int ans = -1;
for(int i = 0;i < m;)
{
int cur_w = a[i].w;
while(i < m && cur_w == a[i].w)
{
int x = a[i].x;
int y = a[i].y;
if(merge(x,y))
now -= 1;
i += 1;
}
if(now == k)
{
ans = cur_w;
break;
}
}
printf("%d\n",ans);
}
return 0;
}
6959 zoto
莫隊
賽后牢騷:
!!!真彩!!!分明離線查詢很容易就想到莫隊的嘛!
題意:給定一些二維左邊上的點,他們的x左邊從1~n,y軸坐標在[0,100000]范圍內。給定一個查詢矩陣(x0,y0) (x1,y1)求在這個矩陣當中有多少個不同的y值大小不同的點。
思路:
離線查詢->莫隊
我們先求出第一次查詢的結果,然后根據第一次查詢結果可以得到相鄰區間查詢的結果,優雅的暴力~
這里只說下怎么由一個區間到另外一個區間...這里是二維的點啊喂!
我們開一個數組num存放當前x0 - x1區間內所有的y值出現的次數(一個桶,num[y]表示y坐標出現的次數)
然后我們對num進行分塊 -> sum數組,每一塊中保存其中不同y的個數
這樣對於每一次的區間x0 - x1的統計點,然后計算分布在y0 - y1就好啦!
計算y0 - y1直接前綴和思想即可(什么,你不知道?就是計算0 ~ y1滿足條件的y的個數 - 0 ~ y0 - 1中滿足條件的y的個數)
區間轉移:
相信你很容易就明白下面這兩個函數:
void add(int x)
{
num[fx[x]] += 1;
if(num[fx[x]] == 1) sum[fx[x]/K] += 1;
}
void del(int x)
{
num[fx[x]] -= 1;
if(num[fx[x]] == 0) sum[fx[x]/K] -= 1;
}
add函數,將當前x軸上的點加入到num,sum數組中
num就不說啦,看sum,由於我們只統計不重復的,所以統計第一次出現的那個y就好了,然后分塊,分塊大小K = sqrt(n)
del函數也是一樣的道理的啦!
然后是計算0~y有多少個不重復的y:
int cal(int y)
{
int ans = 0;
int R = y/K;
for(int i = 0;i < R;i++) ans += sum[i];
for(int i = R*K;i <= y;i++) ans += (num[i] >= 1);
return ans;
}
塊內直接統計sum,離散部分暴力統計即可~
完整代碼:
點擊查看代碼
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int MAXN = 100005;
int K;
struct node
{
int l,r;
int u,d;
int idx;
bool operator < (const node &a)const
{
if(l/K != a.l/K) return l < a.l;
if((l/K)&1) return r > a.r;
return r < a.r;
}
}a[MAXN];
int num[MAXN];//統計對應y下的x的個數
int sum[MAXN];//分塊中的各個值
int ans[MAXN];
int fx[MAXN];
void add(int x)
{
num[fx[x]] += 1;
if(num[fx[x]] == 1) sum[fx[x]/K] += 1;
}
void del(int x)
{
num[fx[x]] -= 1;
if(num[fx[x]] == 0) sum[fx[x]/K] -= 1;
}
int cal(int y)
{
int ans = 0;
int R = y/K;
for(int i = 0;i < R;i++) ans += sum[i];
for(int i = R*K;i <= y;i++) ans += (num[i] >= 1);
return ans;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
memset(num,0,sizeof num);
memset(sum,0,sizeof sum);
int n,m;
scanf("%d %d",&n,&m);
for(int i = 1;i <= n;i++) scanf("%d",&fx[i]);
K = sqrt(n + 0.5);
for(int i = 0;i < m;i++)
scanf("%d %d %d %d",&a[i].l,&a[i].d,&a[i].r,&a[i].u),a[i].idx = i;
sort(a,a + m);
int le,ri;//先暴力算第一個
le = a[0].l,ri = a[0].r;
for(int i = le;i <= ri;i++) add(i);
int now = cal(a[0].u) - cal(a[0].d - 1);
ans[a[0].idx] = now;
for(int i = 1;i < m;i++)
{
while(le < a[i].l)
del(le++);
while(le > a[i].l)
add(--le);
while(ri < a[i].r)
add(++ri);
while(ri > a[i].r)
del(ri--);
now = cal(a[i].u) - cal(a[i].d - 1);
ans[a[i].idx] = now;
}
for(int i = 0;i < m;i++)
printf("%d\n",ans[i]);
}
return 0;
}
未完待續....(其實是太菜了,其他的題還不會)
知識點預告:高斯消元、KD-Tree、數學公式QAQs
