// https://atcoder.jp/contests/agc033/tasks/agc033_d
/*<head>*/
// #include "lib/Template.hpp"
/*</head>*/

/*<body>*/
/* #region header */
/**
 * @file Template.hpp
 * @brief 競技プログラミング用テンプレート
 * @author btk15049
 * @date 2019/05/02
 */
#include <bits/stdc++.h>
using namespace std;

/* #region macro */
#ifdef BTK
#    define DEBUG if (1)
#    define CIN_ONLY if (0)
#else
#    define DEBUG if (0)
#    define CIN_ONLY if (1)
#endif
/** @def
 * ALLマクロ
 */
#define ALL(v) (v).begin(), (v).end()

/** @def
 * 再帰ラムダをするためのマクロ
 */
#define REC(ret, ...) std::function<ret(__VA_ARGS__)>
/* #endregion */

namespace _Template_ {
    /**
     * @brief cin高速化処理を行うための構造体
     * @details CIN_ONLYマクロで動作が変わる
     */
    struct cww {
        cww() {
            CIN_ONLY {
                ios::sync_with_stdio(false);
                cin.tie(0);
            }
        }
    } star;
    /**
     * @brief change min
     * @tparam T 型
     * @param l 参照
     * @param r 非参照
     * @return 更新があればtrue
     */
    template <typename T>
    inline bool chmin(T& l, T r) {
        bool a = l > r;
        if (a) l = r;
        return a;
    }
    /**
     * @brief chminのmax版
     * @see chmin
     */
    template <typename T>
    inline bool chmax(T& l, T r) {
        bool a = l < r;
        if (a) l = r;
        return a;
    }
    /**
     * @brief
     * vectorに直接cin流すためのやつ
     * @tparam T
     * @param is
     * @param v
     * @return istream&
     */
    template <typename T>
    istream& operator>>(istream& is, vector<T>& v) {
        for (auto& it : v) is >> it;
        return is;
    }
    /**
     * @brief
     * rangeを逆向きに操作したいとき用
     * @details
     * ループの範囲は[bg,ed)なので注意
     * @see range
     */
    class reverse_range {
      private:
        struct I {
            int x;
            int operator*() { return x - 1; }
            bool operator!=(I& lhs) { return x > lhs.x; }
            void operator++() { --x; }
        };
        I i, n;

      public:
        reverse_range(int n) : i({0}), n({n}) {}
        reverse_range(int i, int n) : i({i}), n({n}) {}
        I& begin() { return n; }
        I& end() { return i; }
    };
    /**
     * @brief
     * python みたいな range-based for を実現
     * @details
     * ループの範囲は[bg,ed)なので注意
     * !つけると逆向きにループが回る (reverse_range)
     * 空間計算量はO(1)
     * 使わない変数ができて警告が出がちなので，unused_varとかを使って警告消しするとよい
     */
    class range {
      private:
        struct I {
            int x;
            int operator*() { return x; }
            bool operator!=(I& lhs) { return x < lhs.x; }
            void operator++() { ++x; }
        };
        I i, n;

      public:
        range(int n) : i({0}), n({n}) {}
        range(int i, int n) : i({i}), n({n}) {}
        I& begin() { return i; }
        I& end() { return n; }
        reverse_range operator!() { return reverse_range(*i, *n); }
    };
    /**
     * @brief
     * rangeで生まれる使わない変数を消す用（警告消し）
     */
    template <typename T>
    inline T& unused_var(T& v) {
        return v;
    }
    using LL = long long;
} // namespace _Template_
using namespace _Template_;

/* #endregion */
/*</body>*/

int R;
int C;
int grid[212][212];

template <typename T>
class NDArray {
  private:
    T* ptr;
    int sizeAll;
    std::vector<int> dims;
    std::vector<int> slices;
    inline int computeIndex(std::vector<int>&& indices) {
        int index = 0;
        for (int i : range(slices.size())) {
            index += indices[i] * slices[i];
        }
        return index;
    }

    inline int computeIndexRecur(int di) { return di & 0; }

    template <typename... Ts>
    inline int computeIndexRecur(int di, int head, Ts&&... tail) {
        return head * slices[di] + computeIndexRecur(di + 1, tail...);
    }


    template <typename... Ts>
    inline int computeIndex(Ts&&... indices) {
        return computeIndexRecur(0, indices...);
        // return computeIndex({indices...});
    }

  public:
    template <typename... Ts>
    NDArray(T* ptr, Ts&&... params) : ptr(ptr) {
        for (auto const& d : {params...}) {
            dims.push_back(d);
        }
        slices.resize(dims.size());
        slices.back() = 1;
        for (int di : !range(1, dims.size())) {
            slices[di - 1] = slices[di] * dims[di];
        }
        sizeAll = slices.front() * dims.front();
    }

    template <typename... Ts>
    inline T& random_access(Ts&&... indices) {
        return ptr[computeIndex(indices...)];
    }

    template <typename... Ts>
    inline T& operator()(Ts&&... indices) {
        return random_access(indices...);
    }

    /*
        inline T& operator()(int i0, int i1, int i2) {
            return ptr[i0 * slices[0] + i1 * slices[1] + i2 * slices[2]];
        }
    */


    inline void fill(T&& v) { std::fill(ptr, ptr + sizeAll, v); }

    const vector<int>& getDimInfo() { return dims; }
};

namespace solver {
    int mem1[212 * 212 * 212];
    int mem2[212 * 212 * 212];
    void array_clear(NDArray<int>& dp) {
        for (int ri : range(R)) {
            for (int ci : range(C + 1)) {
                for (int rj : range(R + 1)) {
                    dp(ri, ci, rj) = ci;
                }
            }
        }
    }
    void init(NDArray<int>& dp) {
        array_clear(dp);
        // -----
        for (int ri : range(R)) {
            for (int ci : range(C)) {
                int cj = ci;
                while (cj < C && grid[ri][ci] == grid[ri][cj]) {
                    cj++;
                }
                dp(ri, ci, ri + 1) = cj;
            }
        }
        for (int ri : range(R)) {
            for (int ci : range(C)) {
                for (int rj : range(ri + 2, R + 1)) {
                    if (grid[rj - 2][ci] == grid[rj - 1][ci]) {
                        dp(ri, ci, rj) =
                            min(dp(ri, ci, rj - 1), dp(rj - 1, ci, rj));
                    }
                }
            }
        }
    }
    void run() {
        NDArray<int> dp(mem1, R, C + 1, R + 1);
        NDArray<int> nx(mem2, R, C + 1, R + 1);
        init(dp);
        int ret = 0;
        while (dp(0, 0, R) < C) {
            ret++;
            array_clear(nx);
            for (int ri : range(R)) {
                for (int ci : range(C)) {
                    for (int rj : range(ri + 1, R + 1)) {
                        nx(ri, ci, rj) = dp(ri, dp(ri, ci, rj), rj);
                    }
                }
            }
            for (int ri : range(R)) {
                for (int ci : range(C)) {
                    int rm = ri + 1;
                    for (int rj : range(ri + 2, R + 1)) {
                        while (rm + 1 < rj
                               && min(dp(ri, ci, rm), dp(rm, ci, rj))
                                      <= min(dp(ri, ci, rm + 1),
                                             dp(rm + 1, ci, rj))) {
                            rm++;
                        }
                        chmax(nx(ri, ci, rj),
                              min(dp(ri, ci, rm), dp(rm, ci, rj)));
                    }
                }
            }
            swap(dp, nx);
        }

        cout << ret << endl;
    }
} // namespace solver

int main() {
    cin >> R >> C;
    for (int ri : range(R)) {
        string s;
        cin >> s;
        for (int ci : range(C)) {
            grid[ri][ci] = (s[ci] == '#') ? 1 : 0;
        }
    }
    solver::run();
    return 0;
}

Measured memory_bytes: 53632